Add logs function to karmadactl

Signed-off-by: lonelyCZ <531187475@qq.com>
This commit is contained in:
lonelyCZ 2022-04-22 13:52:06 +08:00
parent a3c334c3b1
commit a52bfca0b8
44 changed files with 10638 additions and 0 deletions

1
go.mod
View File

@ -61,6 +61,7 @@ require (
github.com/emicklei/go-restful v2.9.5+incompatible // indirect
github.com/evanphx/json-patch v4.12.0+incompatible // indirect
github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect
github.com/fatih/camelcase v1.0.0 // indirect
github.com/felixge/httpsnoop v1.0.1 // indirect
github.com/fsnotify/fsnotify v1.5.1 // indirect
github.com/fvbommel/sortorder v1.0.1 // indirect

1
go.sum
View File

@ -241,6 +241,7 @@ github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJ
github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4=
github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d h1:105gxyaGwCFad8crR9dcMQWvV9Hvulu6hwUh4tWPJnM=
github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d/go.mod h1:ZZMPRZwes7CROmyNKgQzC3XPs6L/G2EJLHddWejkmf4=
github.com/fatih/camelcase v1.0.0 h1:hxNvNX/xYBp0ovncs8WyWZrOrpBNub/JfaMvbURyft8=
github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=

View File

@ -56,6 +56,7 @@ func NewKarmadaCtlCommand(out io.Writer, cmdUse, parentCommand string) *cobra.Co
rootCmd.AddCommand(NewCmdGet(out, karmadaConfig, parentCommand))
rootCmd.AddCommand(NewCmdTaint(out, karmadaConfig, parentCommand))
rootCmd.AddCommand(NewCmdPromote(out, karmadaConfig, parentCommand))
rootCmd.AddCommand(NewCmdLogs(out, karmadaConfig, parentCommand))
rootCmd.AddCommand(cmdinit.NewCmdInit(out, parentCommand))
return rootCmd

473
pkg/karmadactl/logs.go Normal file
View File

@ -0,0 +1,473 @@
package karmadactl
import (
"context"
"errors"
"fmt"
"io"
"os"
"regexp"
"sync"
"time"
"github.com/spf13/cobra"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/client-go/rest"
cmdutil "k8s.io/kubectl/pkg/cmd/util"
"k8s.io/kubectl/pkg/polymorphichelpers"
karmadaclientset "github.com/karmada-io/karmada/pkg/generated/clientset/versioned"
"github.com/karmada-io/karmada/pkg/karmadactl/options"
"github.com/karmada-io/karmada/pkg/util/gclient"
"github.com/karmada-io/karmada/pkg/util/lifted"
)
const (
defaultPodLogsTimeout = 20 * time.Second
logsUsageStr = "logs [-f] [-p] (POD | TYPE/NAME) [-c CONTAINER] (-C CLUSTER)"
)
var (
selectorTail int64 = 10
logsUsageErrStr = fmt.Sprintf("expected '%s'.\nPOD or TYPE/NAME is a required argument for the logs command", logsUsageStr)
)
// NewCmdLogs new logs command.
func NewCmdLogs(out io.Writer, karmadaConfig KarmadaConfig, parentCommand string) *cobra.Command {
ioStreams := genericclioptions.IOStreams{In: getIn, Out: getOut, ErrOut: getErr}
o := NewCommandLogsOptions(ioStreams, false)
cmd := &cobra.Command{
Use: logsUsageStr,
Short: "Print the logs for a container in a pod in a cluster",
SilenceUsage: true,
Example: getLogsExample(parentCommand),
RunE: func(cmd *cobra.Command, args []string) error {
if err := o.Complete(karmadaConfig, cmd, args); err != nil {
return err
}
if err := o.Validate(); err != nil {
return err
}
if err := o.Run(); err != nil {
return err
}
return nil
},
}
o.GlobalCommandOptions.AddFlags(cmd.Flags())
o.AddFlags(cmd)
return cmd
}
// getLogsExample logs examples by cmd type
func getLogsExample(parentCommand string) string {
example := `
# Return snapshot logs from pod nginx with only one container in cluster(member1)` + "\n" +
fmt.Sprintf("%s logs nginx -C=member1", parentCommand) + `
# Return snapshot logs from pod nginx with multi containers in cluster(member1)` + "\n" +
fmt.Sprintf("%s get logs nginx --all-containers=true -C=member1", parentCommand) + `
# Return snapshot logs from all containers in pods defined by label app=nginx in cluster(member1)` + "\n" +
fmt.Sprintf("%s get logs -l app=nginx --all-containers=true -C=member1", parentCommand) + `
# Return snapshot of previous terminated ruby container logs from pod web-1 in cluster(member1)` + "\n" +
fmt.Sprintf("%s get logs -p -c ruby web-1 -C=member1", parentCommand) + `
# Begin streaming the logs of the ruby container in pod web-1 in cluster(member1)` + "\n" +
fmt.Sprintf("%s logs -f -c ruby web-1 -C=member1", parentCommand) + `
# Begin streaming the logs from all containers in pods defined by label app=nginx in cluster(member1) ` + "\n" +
fmt.Sprintf("%s logs -f -l app=nginx --all-containers=true -C=member1", parentCommand) + `
# Display only the most recent 20 lines of output in pod nginx in cluster(member1) ` + "\n" +
fmt.Sprintf("%s logs --tail=20 nginx -C=member1", parentCommand) + `
# Show all logs from pod nginx written in the last hour in cluster(member1) ` + "\n" +
fmt.Sprintf("%s --since=1h nginx -C=member1", parentCommand)
return example
}
// CommandLogsOptions contains the input to the logs command.
type CommandLogsOptions struct {
// global flags
options.GlobalCommandOptions
Cluster string
Namespace string
ResourceArg string
AllContainers bool
Options runtime.Object
Resources []string
ConsumeRequestFn func(rest.ResponseWrapper, io.Writer) error
// PodLogOptions
SinceTime string
Since time.Duration
Follow bool
Previous bool
Timestamps bool
IgnoreLogErrors bool
LimitBytes int64
Tail int64
Container string
InsecureSkipTLSVerifyBackend bool
// whether or not a container name was given via --container
ContainerNameSpecified bool
Selector string
MaxFollowConcurrency int
Prefix bool
Object runtime.Object
GetPodTimeout time.Duration
RESTClientGetter genericclioptions.RESTClientGetter
LogsForObject polymorphichelpers.LogsForObjectFunc
genericclioptions.IOStreams
TailSpecified bool
containerNameFromRefSpecRegexp *regexp.Regexp
f cmdutil.Factory
}
// NewCommandLogsOptions returns a LogsOptions.
func NewCommandLogsOptions(streams genericclioptions.IOStreams, allContainers bool) *CommandLogsOptions {
return &CommandLogsOptions{
IOStreams: streams,
AllContainers: allContainers,
Tail: -1,
MaxFollowConcurrency: 5,
containerNameFromRefSpecRegexp: regexp.MustCompile(`spec\.(?:initContainers|containers|ephemeralContainers){(.+)}`),
}
}
// AddFlags adds flags to the specified FlagSet.
func (o *CommandLogsOptions) AddFlags(cmd *cobra.Command) {
cmd.Flags().BoolVar(&o.AllContainers, "all-containers", o.AllContainers, "Get all containers' logs in the pod(s).")
cmd.Flags().BoolVarP(&o.Follow, "follow", "f", o.Follow, "Specify if the logs should be streamed.")
cmd.Flags().BoolVar(&o.Timestamps, "timestamps", o.Timestamps, "Include timestamps on each line in the log output")
cmd.Flags().Int64Var(&o.LimitBytes, "limit-bytes", o.LimitBytes, "Maximum bytes of logs to return. Defaults to no limit.")
cmd.Flags().BoolVarP(&o.Previous, "previous", "p", o.Previous, "If true, print the logs for the previous instance of the container in a pod if it exists.")
cmd.Flags().Int64Var(&o.Tail, "tail", o.Tail, "Lines of recent log file to display. Defaults to -1 with no selector, showing all log lines otherwise 10, if a selector is provided.")
cmd.Flags().BoolVar(&o.IgnoreLogErrors, "ignore-errors", o.IgnoreLogErrors, "If watching / following pod logs, allow for any errors that occur to be non-fatal")
cmd.Flags().StringVar(&o.SinceTime, "since-time", o.SinceTime, "Only return logs after a specific date (RFC3339). Defaults to all logs. Only one of since-time / since may be used.")
cmd.Flags().DurationVar(&o.Since, "since", o.Since, "Only return logs newer than a relative duration like 5s, 2m, or 3h. Defaults to all logs. Only one of since-time / since may be used.")
cmd.Flags().StringVarP(&o.Container, "container", "c", o.Container, "Print the logs of this container")
cmd.Flags().BoolVar(&o.InsecureSkipTLSVerifyBackend, "insecure-skip-tls-verify-backend", o.InsecureSkipTLSVerifyBackend,
"Skip verifying the identity of the kubelet that logs are requested from. In theory, an attacker could provide invalid log content back. You might want to use this if your kubelet serving certificates have expired.")
cmdutil.AddPodRunningTimeoutFlag(cmd, defaultPodLogsTimeout)
cmd.Flags().StringVarP(&o.Selector, "selector", "l", o.Selector, "Selector (label query) to filter on.")
cmd.Flags().IntVar(&o.MaxFollowConcurrency, "max-log-requests", o.MaxFollowConcurrency, "Specify maximum number of concurrent logs to follow when using by a selector. Defaults to 5.")
cmd.Flags().BoolVar(&o.Prefix, "prefix", o.Prefix, "Prefix each log line with the log source (pod name and container name)")
cmd.Flags().StringVarP(&o.Namespace, "namespace", "n", "default", "-n=namespace or -n namespace")
cmd.Flags().StringVarP(&o.Cluster, "cluster", "C", "", "-C=member1")
}
// Complete ensures that options are valid and marshals them if necessary
func (o *CommandLogsOptions) Complete(karmadaConfig KarmadaConfig, cmd *cobra.Command, args []string) error {
o.ContainerNameSpecified = cmd.Flag("container").Changed
o.TailSpecified = cmd.Flag("tail").Changed
o.Resources = args
switch len(args) {
case 0:
if len(o.Selector) == 0 {
return cmdutil.UsageErrorf(cmd, "%s", logsUsageErrStr)
}
case 1:
o.ResourceArg = args[0]
if len(o.Selector) != 0 {
return cmdutil.UsageErrorf(cmd, "only a selector (-l) or a POD name is allowed")
}
case 2:
o.ResourceArg = args[0]
o.Container = args[1]
default:
return cmdutil.UsageErrorf(cmd, "%s", logsUsageErrStr)
}
var err error
o.ConsumeRequestFn = lifted.DefaultConsumeRequest
o.GetPodTimeout, err = cmdutil.GetPodRunningTimeoutFlag(cmd)
if err != nil {
return err
}
o.Options, err = o.toLogOptions()
if err != nil {
return err
}
if len(o.Cluster) == 0 {
return fmt.Errorf("must specify a cluster")
}
karmadaRestConfig, err := karmadaConfig.GetRestConfig(o.KarmadaContext, o.KubeConfig)
if err != nil {
return fmt.Errorf("failed to get control plane rest config. context: %s, kube-config: %s, error: %v",
o.KarmadaContext, o.KubeConfig, err)
}
clusterInfo, err := o.getClusterInfo(karmadaRestConfig, o.Cluster)
if err != nil {
return err
}
o.f = getFactory(o.Cluster, clusterInfo)
o.RESTClientGetter = o.f
o.LogsForObject = polymorphichelpers.LogsForObjectFn
if err := o.completeObj(); err != nil {
return err
}
return nil
}
// Validate checks to the LogsOptions to see if there is sufficient information run the command
func (o *CommandLogsOptions) Validate() error {
if len(o.SinceTime) > 0 && o.Since != 0 {
return fmt.Errorf("at most one of `sinceTime` or `sinceSeconds` may be specified")
}
logsOptions, ok := o.Options.(*corev1.PodLogOptions)
if !ok {
return errors.New("unexpected logs options object")
}
if o.AllContainers && len(logsOptions.Container) > 0 {
return fmt.Errorf("--all-containers=true should not be specified with container name %s", logsOptions.Container)
}
if o.ContainerNameSpecified && len(o.Resources) == 2 {
return fmt.Errorf("only one of -c or an inline [CONTAINER] arg is allowed")
}
if o.LimitBytes < 0 {
return fmt.Errorf("--limit-bytes must be greater than 0")
}
if logsOptions.SinceSeconds != nil && *logsOptions.SinceSeconds < int64(0) {
return fmt.Errorf("--since must be greater than 0")
}
if logsOptions.TailLines != nil && *logsOptions.TailLines < -1 {
return fmt.Errorf("--tail must be greater than or equal to -1")
}
return nil
}
// Run retrieves a pod log
func (o *CommandLogsOptions) Run() error {
requests, err := o.LogsForObject(o.RESTClientGetter, o.Object, o.Options, o.GetPodTimeout, o.AllContainers)
if err != nil {
return err
}
if o.Follow && len(requests) > 1 {
if len(requests) > o.MaxFollowConcurrency {
return fmt.Errorf(
"you are attempting to follow %d log streams, but maximum allowed concurrency is %d, use --max-log-requests to increase the limit",
len(requests), o.MaxFollowConcurrency,
)
}
return o.parallelConsumeRequest(requests)
}
return o.sequentialConsumeRequest(requests)
}
func (o *CommandLogsOptions) parallelConsumeRequest(requests map[corev1.ObjectReference]rest.ResponseWrapper) error {
reader, writer := io.Pipe()
wg := &sync.WaitGroup{}
wg.Add(len(requests))
for objRef, request := range requests {
go func(objRef corev1.ObjectReference, request rest.ResponseWrapper) {
defer wg.Done()
out := o.addPrefixIfNeeded(objRef, writer)
if err := o.ConsumeRequestFn(request, out); err != nil {
if !o.IgnoreLogErrors {
_ = writer.CloseWithError(err)
// It's important to return here to propagate the error via the pipe
return
}
fmt.Fprintf(writer, "error: %v\n", err)
}
}(objRef, request)
}
go func() {
wg.Wait()
writer.Close()
}()
_, err := io.Copy(o.Out, reader)
return err
}
func (o *CommandLogsOptions) completeObj() error {
if o.Object == nil {
builder := o.f.NewBuilder().
WithScheme(gclient.NewSchema(), gclient.NewSchema().PrioritizedVersionsAllGroups()...).
NamespaceParam(o.Namespace).DefaultNamespace().
SingleResourceType()
if o.ResourceArg != "" {
builder.ResourceNames("pods", o.ResourceArg)
}
if o.Selector != "" {
builder.ResourceTypes("pods").LabelSelectorParam(o.Selector)
}
infos, err := builder.Do().Infos()
if err != nil {
return err
}
if o.Selector == "" && len(infos) != 1 {
return errors.New("expected a resource")
}
o.Object = infos[0].Object
if o.Selector != "" && len(o.Object.(*corev1.PodList).Items) == 0 {
fmt.Fprintf(o.ErrOut, "No resources found in %s namespace.\n", o.Namespace)
}
}
return nil
}
func (o *CommandLogsOptions) toLogOptions() (*corev1.PodLogOptions, error) {
logOptions := &corev1.PodLogOptions{
Container: o.Container,
Follow: o.Follow,
Previous: o.Previous,
Timestamps: o.Timestamps,
InsecureSkipTLSVerifyBackend: o.InsecureSkipTLSVerifyBackend,
}
if len(o.SinceTime) > 0 {
t, err := lifted.ParseRFC3339(o.SinceTime, metav1.Now)
if err != nil {
return nil, err
}
logOptions.SinceTime = &t
}
if o.LimitBytes != 0 {
logOptions.LimitBytes = &o.LimitBytes
}
if o.Since != 0 {
// round up to the nearest second
sec := int64(o.Since.Round(time.Second))
logOptions.SinceSeconds = &sec
}
if len(o.Selector) > 0 && o.Tail == -1 && !o.TailSpecified {
logOptions.TailLines = &selectorTail
} else if o.Tail != -1 {
logOptions.TailLines = &o.Tail
}
return logOptions, nil
}
func (o *CommandLogsOptions) sequentialConsumeRequest(requests map[corev1.ObjectReference]rest.ResponseWrapper) error {
for objRef, request := range requests {
out := o.addPrefixIfNeeded(objRef, o.Out)
if err := o.ConsumeRequestFn(request, out); err != nil {
if !o.IgnoreLogErrors {
return err
}
fmt.Fprintf(o.Out, "error: %v\n", err)
}
}
return nil
}
func (o *CommandLogsOptions) addPrefixIfNeeded(ref corev1.ObjectReference, writer io.Writer) io.Writer {
if !o.Prefix || ref.FieldPath == "" || ref.Name == "" {
return writer
}
// We rely on ref.FieldPath to contain a reference to a container
// including a container name (not an index) so we can get a container name
// without making an extra API request.
var containerName string
containerNameMatches := o.containerNameFromRefSpecRegexp.FindStringSubmatch(ref.FieldPath)
if len(containerNameMatches) == 2 {
containerName = containerNameMatches[1]
}
prefix := fmt.Sprintf("[pod/%s/%s] ", ref.Name, containerName)
return &prefixingWriter{
prefix: []byte(prefix),
writer: writer,
}
}
// getClusterInfo get information of cluster
func (o *CommandLogsOptions) getClusterInfo(karmadaRestConfig *rest.Config, name string) (map[string]*ClusterInfo, error) {
clusterClient := karmadaclientset.NewForConfigOrDie(karmadaRestConfig).ClusterV1alpha1().Clusters()
// check if the cluster exist in karmada control plane
_, err := clusterClient.Get(context.TODO(), o.Cluster, metav1.GetOptions{})
if err != nil {
return nil, err
}
clusterInfos := make(map[string]*ClusterInfo)
clusterInfos[name] = &ClusterInfo{}
clusterInfos[name].APIEndpoint = karmadaRestConfig.Host + fmt.Sprintf(proxyURL, name)
clusterInfos[name].KubeConfig = o.KubeConfig
clusterInfos[name].Context = o.KarmadaContext
if clusterInfos[name].KubeConfig == "" {
env := os.Getenv("KUBECONFIG")
if env != "" {
clusterInfos[name].KubeConfig = env
} else {
clusterInfos[name].KubeConfig = defaultKubeConfig
}
}
return clusterInfos, nil
}
type prefixingWriter struct {
prefix []byte
writer io.Writer
}
func (pw *prefixingWriter) Write(p []byte) (int, error) {
if len(p) == 0 {
return 0, nil
}
// Perform an "atomic" write of a prefix and p to make sure that it doesn't interleave
// sub-line when used concurrently with io.PipeWrite.
n, err := pw.writer.Write(append(pw.prefix, p...))
if n > len(p) {
// To comply with the io.Writer interface requirements we must
// return a number of bytes written from p (0 <= n <= len(p)),
// so we are ignoring the length of the prefix here.
return len(p), err
}
return n, err
}

70
pkg/util/lifted/logs.go Normal file
View File

@ -0,0 +1,70 @@
/*
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 lifted
import (
"bufio"
"context"
"io"
"time"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/rest"
)
// DefaultConsumeRequest reads the data from request and writes into
// the out writer. It buffers data from requests until the newline or io.EOF
// occurs in the data, so it doesn't interleave logs sub-line
// when running concurrently.
//
// A successful read returns err == nil, not err == io.EOF.
// Because the function is defined to read from request until io.EOF, it does
// not treat an io.EOF as an error to be reported.
func DefaultConsumeRequest(request rest.ResponseWrapper, out io.Writer) error {
readCloser, err := request.Stream(context.TODO())
if err != nil {
return err
}
defer readCloser.Close()
r := bufio.NewReader(readCloser)
for {
bytes, err := r.ReadBytes('\n')
if _, err := out.Write(bytes); err != nil {
return err
}
if err != nil {
if err != io.EOF {
return err
}
return nil
}
}
}
// 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
}

View File

@ -0,0 +1,111 @@
/*
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 lifted
import (
"bytes"
"context"
"errors"
"io"
"io/ioutil"
"strings"
"testing"
"testing/iotest"
restclient "k8s.io/client-go/rest"
)
func TestDefaultConsumeRequest(t *testing.T) {
tests := []struct {
name string
request restclient.ResponseWrapper
expectedErr string
expectedOut string
}{
{
name: "error from request stream",
request: &responseWrapperMock{
err: errors.New("err from the stream"),
},
expectedErr: "err from the stream",
},
{
name: "error while reading",
request: &responseWrapperMock{
data: iotest.TimeoutReader(strings.NewReader("Some data")),
},
expectedErr: iotest.ErrTimeout.Error(),
expectedOut: "Some data",
},
{
name: "read with empty string",
request: &responseWrapperMock{
data: strings.NewReader(""),
},
expectedOut: "",
},
{
name: "read without new lines",
request: &responseWrapperMock{
data: strings.NewReader("some string without a new line"),
},
expectedOut: "some string without a new line",
},
{
name: "read with newlines in the middle",
request: &responseWrapperMock{
data: strings.NewReader("foo\nbar"),
},
expectedOut: "foo\nbar",
},
{
name: "read with newline at the end",
request: &responseWrapperMock{
data: strings.NewReader("foo\n"),
},
expectedOut: "foo\n",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
buf := &bytes.Buffer{}
err := DefaultConsumeRequest(test.request, buf)
if err != nil && !strings.Contains(err.Error(), test.expectedErr) {
t.Errorf("%s: expected to find:\n\t%s\nfound:\n\t%s\n", test.name, test.expectedErr, err.Error())
}
if buf.String() != test.expectedOut {
t.Errorf("%s: did not get expected log content. Got: %s", test.name, buf.String())
}
})
}
}
type responseWrapperMock struct {
data io.Reader
err error
}
func (r *responseWrapperMock) DoRaw(context.Context) ([]byte, error) {
data, _ := ioutil.ReadAll(r.data)
return data, r.err
}
func (r *responseWrapperMock) Stream(context.Context) (io.ReadCloser, error) {
return ioutil.NopCloser(r.data), r.err
}

3
vendor/github.com/fatih/camelcase/.travis.yml generated vendored Normal file
View File

@ -0,0 +1,3 @@
language: go
go: 1.x

20
vendor/github.com/fatih/camelcase/LICENSE.md generated vendored Normal file
View File

@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2015 Fatih Arslan
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

58
vendor/github.com/fatih/camelcase/README.md generated vendored Normal file
View File

@ -0,0 +1,58 @@
# CamelCase [![GoDoc](http://img.shields.io/badge/go-documentation-blue.svg?style=flat-square)](http://godoc.org/github.com/fatih/camelcase) [![Build Status](http://img.shields.io/travis/fatih/camelcase.svg?style=flat-square)](https://travis-ci.org/fatih/camelcase)
CamelCase is a Golang (Go) package to split the words of a camelcase type
string into a slice of words. It can be used to convert a camelcase word (lower
or upper case) into any type of word.
## Splitting rules:
1. If string is not valid UTF-8, return it without splitting as
single item array.
2. Assign all unicode characters into one of 4 sets: lower case
letters, upper case letters, numbers, and all other characters.
3. Iterate through characters of string, introducing splits
between adjacent characters that belong to different sets.
4. Iterate through array of split strings, and if a given string
is upper case:
* if subsequent string is lower case:
* move last character of upper case string to beginning of
lower case string
## Install
```bash
go get github.com/fatih/camelcase
```
## Usage and examples
```go
splitted := camelcase.Split("GolangPackage")
fmt.Println(splitted[0], splitted[1]) // prints: "Golang", "Package"
```
Both lower camel case and upper camel case are supported. For more info please
check: [http://en.wikipedia.org/wiki/CamelCase](http://en.wikipedia.org/wiki/CamelCase)
Below are some example cases:
```
"" => []
"lowercase" => ["lowercase"]
"Class" => ["Class"]
"MyClass" => ["My", "Class"]
"MyC" => ["My", "C"]
"HTML" => ["HTML"]
"PDFLoader" => ["PDF", "Loader"]
"AString" => ["A", "String"]
"SimpleXMLParser" => ["Simple", "XML", "Parser"]
"vimRPCPlugin" => ["vim", "RPC", "Plugin"]
"GL11Version" => ["GL", "11", "Version"]
"99Bottles" => ["99", "Bottles"]
"May5" => ["May", "5"]
"BFG9000" => ["BFG", "9000"]
"BöseÜberraschung" => ["Böse", "Überraschung"]
"Two spaces" => ["Two", " ", "spaces"]
"BadUTF8\xe2\xe2\xa1" => ["BadUTF8\xe2\xe2\xa1"]
```

90
vendor/github.com/fatih/camelcase/camelcase.go generated vendored Normal file
View File

@ -0,0 +1,90 @@
// Package camelcase is a micro package to split the words of a camelcase type
// string into a slice of words.
package camelcase
import (
"unicode"
"unicode/utf8"
)
// Split splits the camelcase word and returns a list of words. It also
// supports digits. Both lower camel case and upper camel case are supported.
// For more info please check: http://en.wikipedia.org/wiki/CamelCase
//
// Examples
//
// "" => [""]
// "lowercase" => ["lowercase"]
// "Class" => ["Class"]
// "MyClass" => ["My", "Class"]
// "MyC" => ["My", "C"]
// "HTML" => ["HTML"]
// "PDFLoader" => ["PDF", "Loader"]
// "AString" => ["A", "String"]
// "SimpleXMLParser" => ["Simple", "XML", "Parser"]
// "vimRPCPlugin" => ["vim", "RPC", "Plugin"]
// "GL11Version" => ["GL", "11", "Version"]
// "99Bottles" => ["99", "Bottles"]
// "May5" => ["May", "5"]
// "BFG9000" => ["BFG", "9000"]
// "BöseÜberraschung" => ["Böse", "Überraschung"]
// "Two spaces" => ["Two", " ", "spaces"]
// "BadUTF8\xe2\xe2\xa1" => ["BadUTF8\xe2\xe2\xa1"]
//
// Splitting rules
//
// 1) If string is not valid UTF-8, return it without splitting as
// single item array.
// 2) Assign all unicode characters into one of 4 sets: lower case
// letters, upper case letters, numbers, and all other characters.
// 3) Iterate through characters of string, introducing splits
// between adjacent characters that belong to different sets.
// 4) Iterate through array of split strings, and if a given string
// is upper case:
// if subsequent string is lower case:
// move last character of upper case string to beginning of
// lower case string
func Split(src string) (entries []string) {
// don't split invalid utf8
if !utf8.ValidString(src) {
return []string{src}
}
entries = []string{}
var runes [][]rune
lastClass := 0
class := 0
// split into fields based on class of unicode character
for _, r := range src {
switch true {
case unicode.IsLower(r):
class = 1
case unicode.IsUpper(r):
class = 2
case unicode.IsDigit(r):
class = 3
default:
class = 4
}
if class == lastClass {
runes[len(runes)-1] = append(runes[len(runes)-1], r)
} else {
runes = append(runes, []rune{r})
}
lastClass = class
}
// handle upper case -> lower case sequences, e.g.
// "PDFL", "oader" -> "PDF", "Loader"
for i := 0; i < len(runes)-1; i++ {
if unicode.IsUpper(runes[i][0]) && unicode.IsLower(runes[i+1][0]) {
runes[i+1] = append([]rune{runes[i][len(runes[i])-1]}, runes[i+1]...)
runes[i] = runes[i][:len(runes[i])-1]
}
}
// construct []string from results
for _, s := range runes {
if len(s) > 0 {
entries = append(entries, string(s))
}
}
return
}

365
vendor/k8s.io/client-go/util/certificate/csr/csr.go generated vendored Normal file
View File

@ -0,0 +1,365 @@
/*
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 csr
import (
"context"
"crypto"
"crypto/x509"
"encoding/pem"
"fmt"
"reflect"
"time"
certificatesv1 "k8s.io/api/certificates/v1"
certificatesv1beta1 "k8s.io/api/certificates/v1beta1"
"k8s.io/apimachinery/pkg/api/errors"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/apimachinery/pkg/watch"
clientset "k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/cache"
watchtools "k8s.io/client-go/tools/watch"
certutil "k8s.io/client-go/util/cert"
"k8s.io/klog/v2"
"k8s.io/utils/pointer"
)
// RequestCertificate will either use an existing (if this process has run
// before but not to completion) or create a certificate signing request using the
// PEM encoded CSR and send it to API server. An optional requestedDuration may be passed
// to set the spec.expirationSeconds field on the CSR to control the lifetime of the issued
// certificate. This is not guaranteed as the signer may choose to ignore the request.
func RequestCertificate(client clientset.Interface, csrData []byte, name, signerName string, requestedDuration *time.Duration, usages []certificatesv1.KeyUsage, privateKey interface{}) (reqName string, reqUID types.UID, err error) {
csr := &certificatesv1.CertificateSigningRequest{
// Username, UID, Groups will be injected by API server.
TypeMeta: metav1.TypeMeta{Kind: "CertificateSigningRequest"},
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
Spec: certificatesv1.CertificateSigningRequestSpec{
Request: csrData,
Usages: usages,
SignerName: signerName,
},
}
if len(csr.Name) == 0 {
csr.GenerateName = "csr-"
}
if requestedDuration != nil {
csr.Spec.ExpirationSeconds = DurationToExpirationSeconds(*requestedDuration)
}
reqName, reqUID, err = create(client, csr)
switch {
case err == nil:
return reqName, reqUID, err
case errors.IsAlreadyExists(err) && len(name) > 0:
klog.Infof("csr for this node already exists, reusing")
req, err := get(client, name)
if err != nil {
return "", "", formatError("cannot retrieve certificate signing request: %v", err)
}
if err := ensureCompatible(req, csr, privateKey); err != nil {
return "", "", fmt.Errorf("retrieved csr is not compatible: %v", err)
}
klog.Infof("csr for this node is still valid")
return req.Name, req.UID, nil
default:
return "", "", formatError("cannot create certificate signing request: %v", err)
}
}
func DurationToExpirationSeconds(duration time.Duration) *int32 {
return pointer.Int32(int32(duration / time.Second))
}
func ExpirationSecondsToDuration(expirationSeconds int32) time.Duration {
return time.Duration(expirationSeconds) * time.Second
}
func get(client clientset.Interface, name string) (*certificatesv1.CertificateSigningRequest, error) {
v1req, v1err := client.CertificatesV1().CertificateSigningRequests().Get(context.TODO(), name, metav1.GetOptions{})
if v1err == nil || !apierrors.IsNotFound(v1err) {
return v1req, v1err
}
v1beta1req, v1beta1err := client.CertificatesV1beta1().CertificateSigningRequests().Get(context.TODO(), name, metav1.GetOptions{})
if v1beta1err != nil {
return nil, v1beta1err
}
v1req = &certificatesv1.CertificateSigningRequest{
ObjectMeta: v1beta1req.ObjectMeta,
Spec: certificatesv1.CertificateSigningRequestSpec{
Request: v1beta1req.Spec.Request,
},
}
if v1beta1req.Spec.SignerName != nil {
v1req.Spec.SignerName = *v1beta1req.Spec.SignerName
}
for _, usage := range v1beta1req.Spec.Usages {
v1req.Spec.Usages = append(v1req.Spec.Usages, certificatesv1.KeyUsage(usage))
}
return v1req, nil
}
func create(client clientset.Interface, csr *certificatesv1.CertificateSigningRequest) (reqName string, reqUID types.UID, err error) {
// only attempt a create via v1 if we specified signerName and usages and are not using the legacy unknown signerName
if len(csr.Spec.Usages) > 0 && len(csr.Spec.SignerName) > 0 && csr.Spec.SignerName != "kubernetes.io/legacy-unknown" {
v1req, v1err := client.CertificatesV1().CertificateSigningRequests().Create(context.TODO(), csr, metav1.CreateOptions{})
switch {
case v1err != nil && apierrors.IsNotFound(v1err):
// v1 CSR API was not found, continue to try v1beta1
case v1err != nil:
// other creation error
return "", "", v1err
default:
// success
return v1req.Name, v1req.UID, v1err
}
}
// convert relevant bits to v1beta1
v1beta1csr := &certificatesv1beta1.CertificateSigningRequest{
ObjectMeta: csr.ObjectMeta,
Spec: certificatesv1beta1.CertificateSigningRequestSpec{
SignerName: &csr.Spec.SignerName,
Request: csr.Spec.Request,
},
}
for _, usage := range csr.Spec.Usages {
v1beta1csr.Spec.Usages = append(v1beta1csr.Spec.Usages, certificatesv1beta1.KeyUsage(usage))
}
// create v1beta1
v1beta1req, v1beta1err := client.CertificatesV1beta1().CertificateSigningRequests().Create(context.TODO(), v1beta1csr, metav1.CreateOptions{})
if v1beta1err != nil {
return "", "", v1beta1err
}
return v1beta1req.Name, v1beta1req.UID, nil
}
// WaitForCertificate waits for a certificate to be issued until timeout, or returns an error.
func WaitForCertificate(ctx context.Context, client clientset.Interface, reqName string, reqUID types.UID) (certData []byte, err error) {
fieldSelector := fields.OneTermEqualSelector("metadata.name", reqName).String()
var lw *cache.ListWatch
var obj runtime.Object
for {
// see if the v1 API is available
if _, err := client.CertificatesV1().CertificateSigningRequests().List(ctx, metav1.ListOptions{FieldSelector: fieldSelector}); err == nil {
// watch v1 objects
obj = &certificatesv1.CertificateSigningRequest{}
lw = &cache.ListWatch{
ListFunc: func(options metav1.ListOptions) (runtime.Object, error) {
options.FieldSelector = fieldSelector
return client.CertificatesV1().CertificateSigningRequests().List(ctx, options)
},
WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) {
options.FieldSelector = fieldSelector
return client.CertificatesV1().CertificateSigningRequests().Watch(ctx, options)
},
}
break
} else {
klog.V(2).Infof("error fetching v1 certificate signing request: %v", err)
}
// return if we've timed out
if err := ctx.Err(); err != nil {
return nil, wait.ErrWaitTimeout
}
// see if the v1beta1 API is available
if _, err := client.CertificatesV1beta1().CertificateSigningRequests().List(ctx, metav1.ListOptions{FieldSelector: fieldSelector}); err == nil {
// watch v1beta1 objects
obj = &certificatesv1beta1.CertificateSigningRequest{}
lw = &cache.ListWatch{
ListFunc: func(options metav1.ListOptions) (runtime.Object, error) {
options.FieldSelector = fieldSelector
return client.CertificatesV1beta1().CertificateSigningRequests().List(ctx, options)
},
WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) {
options.FieldSelector = fieldSelector
return client.CertificatesV1beta1().CertificateSigningRequests().Watch(ctx, options)
},
}
break
} else {
klog.V(2).Infof("error fetching v1beta1 certificate signing request: %v", err)
}
// return if we've timed out
if err := ctx.Err(); err != nil {
return nil, wait.ErrWaitTimeout
}
// wait and try again
time.Sleep(time.Second)
}
var issuedCertificate []byte
_, err = watchtools.UntilWithSync(
ctx,
lw,
obj,
nil,
func(event watch.Event) (bool, error) {
switch event.Type {
case watch.Modified, watch.Added:
case watch.Deleted:
return false, fmt.Errorf("csr %q was deleted", reqName)
default:
return false, nil
}
switch csr := event.Object.(type) {
case *certificatesv1.CertificateSigningRequest:
if csr.UID != reqUID {
return false, fmt.Errorf("csr %q changed UIDs", csr.Name)
}
approved := false
for _, c := range csr.Status.Conditions {
if c.Type == certificatesv1.CertificateDenied {
return false, fmt.Errorf("certificate signing request is denied, reason: %v, message: %v", c.Reason, c.Message)
}
if c.Type == certificatesv1.CertificateFailed {
return false, fmt.Errorf("certificate signing request failed, reason: %v, message: %v", c.Reason, c.Message)
}
if c.Type == certificatesv1.CertificateApproved {
approved = true
}
}
if approved {
if len(csr.Status.Certificate) > 0 {
klog.V(2).Infof("certificate signing request %s is issued", csr.Name)
issuedCertificate = csr.Status.Certificate
return true, nil
}
klog.V(2).Infof("certificate signing request %s is approved, waiting to be issued", csr.Name)
}
case *certificatesv1beta1.CertificateSigningRequest:
if csr.UID != reqUID {
return false, fmt.Errorf("csr %q changed UIDs", csr.Name)
}
approved := false
for _, c := range csr.Status.Conditions {
if c.Type == certificatesv1beta1.CertificateDenied {
return false, fmt.Errorf("certificate signing request is denied, reason: %v, message: %v", c.Reason, c.Message)
}
if c.Type == certificatesv1beta1.CertificateFailed {
return false, fmt.Errorf("certificate signing request failed, reason: %v, message: %v", c.Reason, c.Message)
}
if c.Type == certificatesv1beta1.CertificateApproved {
approved = true
}
}
if approved {
if len(csr.Status.Certificate) > 0 {
klog.V(2).Infof("certificate signing request %s is issued", csr.Name)
issuedCertificate = csr.Status.Certificate
return true, nil
}
klog.V(2).Infof("certificate signing request %s is approved, waiting to be issued", csr.Name)
}
default:
return false, fmt.Errorf("unexpected type received: %T", event.Object)
}
return false, nil
},
)
if err == wait.ErrWaitTimeout {
return nil, wait.ErrWaitTimeout
}
if err != nil {
return nil, formatError("cannot watch on the certificate signing request: %v", err)
}
return issuedCertificate, nil
}
// ensureCompatible ensures that a CSR object is compatible with an original CSR
func ensureCompatible(new, orig *certificatesv1.CertificateSigningRequest, privateKey interface{}) error {
newCSR, err := parseCSR(new.Spec.Request)
if err != nil {
return fmt.Errorf("unable to parse new csr: %v", err)
}
origCSR, err := parseCSR(orig.Spec.Request)
if err != nil {
return fmt.Errorf("unable to parse original csr: %v", err)
}
if !reflect.DeepEqual(newCSR.Subject, origCSR.Subject) {
return fmt.Errorf("csr subjects differ: new: %#v, orig: %#v", newCSR.Subject, origCSR.Subject)
}
if len(new.Spec.SignerName) > 0 && len(orig.Spec.SignerName) > 0 && new.Spec.SignerName != orig.Spec.SignerName {
return fmt.Errorf("csr signerNames differ: new %q, orig: %q", new.Spec.SignerName, orig.Spec.SignerName)
}
signer, ok := privateKey.(crypto.Signer)
if !ok {
return fmt.Errorf("privateKey is not a signer")
}
newCSR.PublicKey = signer.Public()
if err := newCSR.CheckSignature(); err != nil {
return fmt.Errorf("error validating signature new CSR against old key: %v", err)
}
if len(new.Status.Certificate) > 0 {
certs, err := certutil.ParseCertsPEM(new.Status.Certificate)
if err != nil {
return fmt.Errorf("error parsing signed certificate for CSR: %v", err)
}
now := time.Now()
for _, cert := range certs {
if now.After(cert.NotAfter) {
return fmt.Errorf("one of the certificates for the CSR has expired: %s", cert.NotAfter)
}
}
}
return nil
}
// formatError preserves the type of an API message but alters the message. Expects
// a single argument format string, and returns the wrapped error.
func formatError(format string, err error) error {
if s, ok := err.(errors.APIStatus); ok {
se := &errors.StatusError{ErrStatus: s.Status()}
se.ErrStatus.Message = fmt.Sprintf(format, se.ErrStatus.Message)
return se
}
return fmt.Errorf(format, err)
}
// parseCSR extracts the CSR from the API object and decodes it.
func parseCSR(pemData []byte) (*x509.CertificateRequest, error) {
// extract PEM from request object
block, _ := pem.Decode(pemData)
if block == nil || block.Type != "CERTIFICATE REQUEST" {
return nil, fmt.Errorf("PEM block type must be CERTIFICATE REQUEST")
}
return x509.ParseCertificateRequest(block.Bytes)
}

75
vendor/k8s.io/kubectl/pkg/apps/kind_visitor.go generated vendored Normal file
View File

@ -0,0 +1,75 @@
/*
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 apps
import (
"fmt"
"k8s.io/apimachinery/pkg/runtime/schema"
)
// KindVisitor is used with GroupKindElement to call a particular function depending on the
// Kind of a schema.GroupKind
type KindVisitor interface {
VisitDaemonSet(kind GroupKindElement)
VisitDeployment(kind GroupKindElement)
VisitJob(kind GroupKindElement)
VisitPod(kind GroupKindElement)
VisitReplicaSet(kind GroupKindElement)
VisitReplicationController(kind GroupKindElement)
VisitStatefulSet(kind GroupKindElement)
VisitCronJob(kind GroupKindElement)
}
// GroupKindElement defines a Kubernetes API group elem
type GroupKindElement schema.GroupKind
// Accept calls the Visit method on visitor that corresponds to elem's Kind
func (elem GroupKindElement) Accept(visitor KindVisitor) error {
switch {
case elem.GroupMatch("apps", "extensions") && elem.Kind == "DaemonSet":
visitor.VisitDaemonSet(elem)
case elem.GroupMatch("apps", "extensions") && elem.Kind == "Deployment":
visitor.VisitDeployment(elem)
case elem.GroupMatch("batch") && elem.Kind == "Job":
visitor.VisitJob(elem)
case elem.GroupMatch("", "core") && elem.Kind == "Pod":
visitor.VisitPod(elem)
case elem.GroupMatch("apps", "extensions") && elem.Kind == "ReplicaSet":
visitor.VisitReplicaSet(elem)
case elem.GroupMatch("", "core") && elem.Kind == "ReplicationController":
visitor.VisitReplicationController(elem)
case elem.GroupMatch("apps") && elem.Kind == "StatefulSet":
visitor.VisitStatefulSet(elem)
case elem.GroupMatch("batch") && elem.Kind == "CronJob":
visitor.VisitCronJob(elem)
default:
return fmt.Errorf("no visitor method exists for %v", elem)
}
return nil
}
// GroupMatch returns true if and only if elem's group matches one
// of the group arguments
func (elem GroupKindElement) GroupMatch(groups ...string) bool {
for _, g := range groups {
if elem.Group == g {
return true
}
}
return false
}

104
vendor/k8s.io/kubectl/pkg/cmd/util/podcmd/podcmd.go generated vendored Normal file
View File

@ -0,0 +1,104 @@
/*
Copyright 2021 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 podcmd
import (
"fmt"
"io"
"strings"
v1 "k8s.io/api/core/v1"
"k8s.io/klog/v2"
)
// DefaultContainerAnnotationName is an annotation name that can be used to preselect the interesting container
// from a pod when running kubectl.
const DefaultContainerAnnotationName = "kubectl.kubernetes.io/default-container"
// FindContainerByName selects the named container from the spec of
// the provided pod or return nil if no such container exists.
func FindContainerByName(pod *v1.Pod, name string) (*v1.Container, string) {
for i := range pod.Spec.Containers {
if pod.Spec.Containers[i].Name == name {
return &pod.Spec.Containers[i], fmt.Sprintf("spec.containers{%s}", name)
}
}
for i := range pod.Spec.InitContainers {
if pod.Spec.InitContainers[i].Name == name {
return &pod.Spec.InitContainers[i], fmt.Sprintf("spec.initContainers{%s}", name)
}
}
for i := range pod.Spec.EphemeralContainers {
if pod.Spec.EphemeralContainers[i].Name == name {
return (*v1.Container)(&pod.Spec.EphemeralContainers[i].EphemeralContainerCommon), fmt.Sprintf("spec.ephemeralContainers{%s}", name)
}
}
return nil, ""
}
// FindOrDefaultContainerByName defaults a container for a pod to the first container if any
// exists, or returns an error. It will print a message to the user indicating a default was
// selected if there was more than one container.
func FindOrDefaultContainerByName(pod *v1.Pod, name string, quiet bool, warn io.Writer) (*v1.Container, error) {
var container *v1.Container
if len(name) > 0 {
container, _ = FindContainerByName(pod, name)
if container == nil {
return nil, fmt.Errorf("container %s not found in pod %s", name, pod.Name)
}
return container, nil
}
// this should never happen, but just in case
if len(pod.Spec.Containers) == 0 {
return nil, fmt.Errorf("pod %s/%s does not have any containers", pod.Namespace, pod.Name)
}
// read the default container the annotation as per
// https://github.com/kubernetes/enhancements/tree/master/keps/sig-cli/2227-kubectl-default-container
if name := pod.Annotations[DefaultContainerAnnotationName]; len(name) > 0 {
if container, _ = FindContainerByName(pod, name); container != nil {
klog.V(4).Infof("Defaulting container name from annotation %s", container.Name)
return container, nil
}
klog.V(4).Infof("Default container name from annotation %s was not found in the pod", name)
}
// pick the first container as per existing behavior
container = &pod.Spec.Containers[0]
if !quiet && (len(pod.Spec.Containers) > 1 || len(pod.Spec.InitContainers) > 0 || len(pod.Spec.EphemeralContainers) > 0) {
fmt.Fprintf(warn, "Defaulted container %q out of: %s\n", container.Name, allContainerNames(pod))
}
klog.V(4).Infof("Defaulting container name to %s", container.Name)
return &pod.Spec.Containers[0], nil
}
func allContainerNames(pod *v1.Pod) string {
var containers []string
for _, container := range pod.Spec.Containers {
containers = append(containers, container.Name)
}
for _, container := range pod.Spec.EphemeralContainers {
containers = append(containers, fmt.Sprintf("%s (ephem)", container.Name))
}
for _, container := range pod.Spec.InitContainers {
containers = append(containers, fmt.Sprintf("%s (init)", container.Name))
}
return strings.Join(containers, ", ")
}

5553
vendor/k8s.io/kubectl/pkg/describe/describe.go generated vendored Normal file

File diff suppressed because it is too large Load Diff

72
vendor/k8s.io/kubectl/pkg/describe/interface.go generated vendored Normal file
View File

@ -0,0 +1,72 @@
/*
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 describe
import (
"fmt"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/cli-runtime/pkg/genericclioptions"
)
const (
// LoadBalancerWidth is the width how we describe load balancer
LoadBalancerWidth = 16
// LabelNodeRolePrefix is a label prefix for node roles
// It's copied over to here until it's merged in core: https://github.com/kubernetes/kubernetes/pull/39112
LabelNodeRolePrefix = "node-role.kubernetes.io/"
// NodeLabelRole specifies the role of a node
NodeLabelRole = "kubernetes.io/role"
)
// DescriberFunc gives a way to display the specified RESTMapping type
type DescriberFunc func(restClientGetter genericclioptions.RESTClientGetter, mapping *meta.RESTMapping) (ResourceDescriber, error)
// ResourceDescriber generates output for the named resource or an error
// if the output could not be generated. Implementers typically
// abstract the retrieval of the named object from a remote server.
type ResourceDescriber interface {
Describe(namespace, name string, describerSettings DescriberSettings) (output string, err error)
}
// DescriberSettings holds display configuration for each object
// describer to control what is printed.
type DescriberSettings struct {
ShowEvents bool
ChunkSize int64
}
// ObjectDescriber is an interface for displaying arbitrary objects with extra
// information. Use when an object is in hand (on disk, or already retrieved).
// Implementers may ignore the additional information passed on extra, or use it
// by default. ObjectDescribers may return ErrNoDescriber if no suitable describer
// is found.
type ObjectDescriber interface {
DescribeObject(object interface{}, extra ...interface{}) (output string, err error)
}
// ErrNoDescriber is a structured error indicating the provided object or objects
// cannot be described.
type ErrNoDescriber struct {
Types []string
}
// Error implements the error interface.
func (e ErrNoDescriber) Error() string {
return fmt.Sprintf("no describer has been defined for %v", e.Types)
}

View File

@ -0,0 +1,54 @@
/*
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 polymorphichelpers
import (
"fmt"
"sort"
"time"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/cli-runtime/pkg/genericclioptions"
corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
"k8s.io/kubectl/pkg/util/podutils"
)
// attachablePodForObject returns the pod to which to attach given an object.
func attachablePodForObject(restClientGetter genericclioptions.RESTClientGetter, object runtime.Object, timeout time.Duration) (*corev1.Pod, error) {
switch t := object.(type) {
case *corev1.Pod:
return t, nil
}
clientConfig, err := restClientGetter.ToRESTConfig()
if err != nil {
return nil, err
}
clientset, err := corev1client.NewForConfig(clientConfig)
if err != nil {
return nil, err
}
namespace, selector, err := SelectorsForObject(object)
if err != nil {
return nil, fmt.Errorf("cannot attach to %T: %v", object, err)
}
sortBy := func(pods []*corev1.Pod) sort.Interface { return sort.Reverse(podutils.ActivePods(pods)) }
pod, _, err := GetFirstPod(clientset, namespace, selector.String(), timeout, sortBy)
return pod, err
}

View File

@ -0,0 +1,44 @@
/*
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 polymorphichelpers
import (
"fmt"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
extensionsv1beta1 "k8s.io/api/extensions/v1beta1"
"k8s.io/apimachinery/pkg/runtime/schema"
)
// Check whether the kind of resources could be exposed
func canBeExposed(kind schema.GroupKind) error {
switch kind {
case
corev1.SchemeGroupVersion.WithKind("ReplicationController").GroupKind(),
corev1.SchemeGroupVersion.WithKind("Service").GroupKind(),
corev1.SchemeGroupVersion.WithKind("Pod").GroupKind(),
appsv1.SchemeGroupVersion.WithKind("Deployment").GroupKind(),
appsv1.SchemeGroupVersion.WithKind("ReplicaSet").GroupKind(),
extensionsv1beta1.SchemeGroupVersion.WithKind("Deployment").GroupKind(),
extensionsv1beta1.SchemeGroupVersion.WithKind("ReplicaSet").GroupKind():
// nothing to do here
default:
return fmt.Errorf("cannot expose a %s", kind)
}
return nil
}

191
vendor/k8s.io/kubectl/pkg/polymorphichelpers/helpers.go generated vendored Normal file
View File

@ -0,0 +1,191 @@
/*
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 polymorphichelpers
import (
"context"
"fmt"
"sort"
"time"
appsv1 "k8s.io/api/apps/v1"
appsv1beta1 "k8s.io/api/apps/v1beta1"
appsv1beta2 "k8s.io/api/apps/v1beta2"
batchv1 "k8s.io/api/batch/v1"
corev1 "k8s.io/api/core/v1"
extensionsv1beta1 "k8s.io/api/extensions/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/watch"
coreclient "k8s.io/client-go/kubernetes/typed/core/v1"
watchtools "k8s.io/client-go/tools/watch"
)
// GetFirstPod returns a pod matching the namespace and label selector
// and the number of all pods that match the label selector.
func GetFirstPod(client coreclient.PodsGetter, namespace string, selector string, timeout time.Duration, sortBy func([]*corev1.Pod) sort.Interface) (*corev1.Pod, int, error) {
options := metav1.ListOptions{LabelSelector: selector}
podList, err := client.Pods(namespace).List(context.TODO(), options)
if err != nil {
return nil, 0, err
}
pods := []*corev1.Pod{}
for i := range podList.Items {
pod := podList.Items[i]
pods = append(pods, &pod)
}
if len(pods) > 0 {
sort.Sort(sortBy(pods))
return pods[0], len(podList.Items), nil
}
// Watch until we observe a pod
options.ResourceVersion = podList.ResourceVersion
w, err := client.Pods(namespace).Watch(context.TODO(), options)
if err != nil {
return nil, 0, err
}
defer w.Stop()
condition := func(event watch.Event) (bool, error) {
return event.Type == watch.Added || event.Type == watch.Modified, nil
}
ctx, cancel := watchtools.ContextWithOptionalTimeout(context.Background(), timeout)
defer cancel()
event, err := watchtools.UntilWithoutRetry(ctx, w, condition)
if err != nil {
return nil, 0, err
}
pod, ok := event.Object.(*corev1.Pod)
if !ok {
return nil, 0, fmt.Errorf("%#v is not a pod event", event)
}
return pod, 1, nil
}
// SelectorsForObject returns the pod label selector for a given object
func SelectorsForObject(object runtime.Object) (namespace string, selector labels.Selector, err error) {
switch t := object.(type) {
case *extensionsv1beta1.ReplicaSet:
namespace = t.Namespace
selector, err = metav1.LabelSelectorAsSelector(t.Spec.Selector)
if err != nil {
return "", nil, fmt.Errorf("invalid label selector: %v", err)
}
case *appsv1.ReplicaSet:
namespace = t.Namespace
selector, err = metav1.LabelSelectorAsSelector(t.Spec.Selector)
if err != nil {
return "", nil, fmt.Errorf("invalid label selector: %v", err)
}
case *appsv1beta2.ReplicaSet:
namespace = t.Namespace
selector, err = metav1.LabelSelectorAsSelector(t.Spec.Selector)
if err != nil {
return "", nil, fmt.Errorf("invalid label selector: %v", err)
}
case *corev1.ReplicationController:
namespace = t.Namespace
selector = labels.SelectorFromSet(t.Spec.Selector)
case *appsv1.StatefulSet:
namespace = t.Namespace
selector, err = metav1.LabelSelectorAsSelector(t.Spec.Selector)
if err != nil {
return "", nil, fmt.Errorf("invalid label selector: %v", err)
}
case *appsv1beta1.StatefulSet:
namespace = t.Namespace
selector, err = metav1.LabelSelectorAsSelector(t.Spec.Selector)
if err != nil {
return "", nil, fmt.Errorf("invalid label selector: %v", err)
}
case *appsv1beta2.StatefulSet:
namespace = t.Namespace
selector, err = metav1.LabelSelectorAsSelector(t.Spec.Selector)
if err != nil {
return "", nil, fmt.Errorf("invalid label selector: %v", err)
}
case *extensionsv1beta1.DaemonSet:
namespace = t.Namespace
selector, err = metav1.LabelSelectorAsSelector(t.Spec.Selector)
if err != nil {
return "", nil, fmt.Errorf("invalid label selector: %v", err)
}
case *appsv1.DaemonSet:
namespace = t.Namespace
selector, err = metav1.LabelSelectorAsSelector(t.Spec.Selector)
if err != nil {
return "", nil, fmt.Errorf("invalid label selector: %v", err)
}
case *appsv1beta2.DaemonSet:
namespace = t.Namespace
selector, err = metav1.LabelSelectorAsSelector(t.Spec.Selector)
if err != nil {
return "", nil, fmt.Errorf("invalid label selector: %v", err)
}
case *extensionsv1beta1.Deployment:
namespace = t.Namespace
selector, err = metav1.LabelSelectorAsSelector(t.Spec.Selector)
if err != nil {
return "", nil, fmt.Errorf("invalid label selector: %v", err)
}
case *appsv1.Deployment:
namespace = t.Namespace
selector, err = metav1.LabelSelectorAsSelector(t.Spec.Selector)
if err != nil {
return "", nil, fmt.Errorf("invalid label selector: %v", err)
}
case *appsv1beta1.Deployment:
namespace = t.Namespace
selector, err = metav1.LabelSelectorAsSelector(t.Spec.Selector)
if err != nil {
return "", nil, fmt.Errorf("invalid label selector: %v", err)
}
case *appsv1beta2.Deployment:
namespace = t.Namespace
selector, err = metav1.LabelSelectorAsSelector(t.Spec.Selector)
if err != nil {
return "", nil, fmt.Errorf("invalid label selector: %v", err)
}
case *batchv1.Job:
namespace = t.Namespace
selector, err = metav1.LabelSelectorAsSelector(t.Spec.Selector)
if err != nil {
return "", nil, fmt.Errorf("invalid label selector: %v", err)
}
case *corev1.Service:
namespace = t.Namespace
if t.Spec.Selector == nil || len(t.Spec.Selector) == 0 {
return "", nil, fmt.Errorf("invalid service '%s': Service is defined without a selector", t.Name)
}
selector = labels.SelectorFromSet(t.Spec.Selector)
default:
return "", nil, fmt.Errorf("selector for %T not implemented", object)
}
return namespace, selector, nil
}

410
vendor/k8s.io/kubectl/pkg/polymorphichelpers/history.go generated vendored Normal file
View File

@ -0,0 +1,410 @@
/*
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 polymorphichelpers
import (
"bytes"
"context"
"fmt"
"io"
"text/tabwriter"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/json"
"k8s.io/apimachinery/pkg/util/strategicpatch"
"k8s.io/client-go/kubernetes"
clientappsv1 "k8s.io/client-go/kubernetes/typed/apps/v1"
"k8s.io/kubectl/pkg/apps"
"k8s.io/kubectl/pkg/describe"
deploymentutil "k8s.io/kubectl/pkg/util/deployment"
sliceutil "k8s.io/kubectl/pkg/util/slice"
)
const (
ChangeCauseAnnotation = "kubernetes.io/change-cause"
)
// HistoryViewer provides an interface for resources have historical information.
type HistoryViewer interface {
ViewHistory(namespace, name string, revision int64) (string, error)
}
type HistoryVisitor struct {
clientset kubernetes.Interface
result HistoryViewer
}
func (v *HistoryVisitor) VisitDeployment(elem apps.GroupKindElement) {
v.result = &DeploymentHistoryViewer{v.clientset}
}
func (v *HistoryVisitor) VisitStatefulSet(kind apps.GroupKindElement) {
v.result = &StatefulSetHistoryViewer{v.clientset}
}
func (v *HistoryVisitor) VisitDaemonSet(kind apps.GroupKindElement) {
v.result = &DaemonSetHistoryViewer{v.clientset}
}
func (v *HistoryVisitor) VisitJob(kind apps.GroupKindElement) {}
func (v *HistoryVisitor) VisitPod(kind apps.GroupKindElement) {}
func (v *HistoryVisitor) VisitReplicaSet(kind apps.GroupKindElement) {}
func (v *HistoryVisitor) VisitReplicationController(kind apps.GroupKindElement) {}
func (v *HistoryVisitor) VisitCronJob(kind apps.GroupKindElement) {}
// HistoryViewerFor returns an implementation of HistoryViewer interface for the given schema kind
func HistoryViewerFor(kind schema.GroupKind, c kubernetes.Interface) (HistoryViewer, error) {
elem := apps.GroupKindElement(kind)
visitor := &HistoryVisitor{
clientset: c,
}
// Determine which HistoryViewer we need here
err := elem.Accept(visitor)
if err != nil {
return nil, fmt.Errorf("error retrieving history for %q, %v", kind.String(), err)
}
if visitor.result == nil {
return nil, fmt.Errorf("no history viewer has been implemented for %q", kind.String())
}
return visitor.result, nil
}
type DeploymentHistoryViewer struct {
c kubernetes.Interface
}
// ViewHistory returns a revision-to-replicaset map as the revision history of a deployment
// TODO: this should be a describer
func (h *DeploymentHistoryViewer) ViewHistory(namespace, name string, revision int64) (string, error) {
versionedAppsClient := h.c.AppsV1()
deployment, err := versionedAppsClient.Deployments(namespace).Get(context.TODO(), name, metav1.GetOptions{})
if err != nil {
return "", fmt.Errorf("failed to retrieve deployment %s: %v", name, err)
}
_, allOldRSs, newRS, err := deploymentutil.GetAllReplicaSets(deployment, versionedAppsClient)
if err != nil {
return "", fmt.Errorf("failed to retrieve replica sets from deployment %s: %v", name, err)
}
allRSs := allOldRSs
if newRS != nil {
allRSs = append(allRSs, newRS)
}
historyInfo := make(map[int64]*corev1.PodTemplateSpec)
for _, rs := range allRSs {
v, err := deploymentutil.Revision(rs)
if err != nil {
continue
}
historyInfo[v] = &rs.Spec.Template
changeCause := getChangeCause(rs)
if historyInfo[v].Annotations == nil {
historyInfo[v].Annotations = make(map[string]string)
}
if len(changeCause) > 0 {
historyInfo[v].Annotations[ChangeCauseAnnotation] = changeCause
}
}
if len(historyInfo) == 0 {
return "No rollout history found.", nil
}
if revision > 0 {
// Print details of a specific revision
template, ok := historyInfo[revision]
if !ok {
return "", fmt.Errorf("unable to find the specified revision")
}
return printTemplate(template)
}
// Sort the revisionToChangeCause map by revision
revisions := make([]int64, 0, len(historyInfo))
for r := range historyInfo {
revisions = append(revisions, r)
}
sliceutil.SortInts64(revisions)
return tabbedString(func(out io.Writer) error {
fmt.Fprintf(out, "REVISION\tCHANGE-CAUSE\n")
for _, r := range revisions {
// Find the change-cause of revision r
changeCause := historyInfo[r].Annotations[ChangeCauseAnnotation]
if len(changeCause) == 0 {
changeCause = "<none>"
}
fmt.Fprintf(out, "%d\t%s\n", r, changeCause)
}
return nil
})
}
func printTemplate(template *corev1.PodTemplateSpec) (string, error) {
buf := bytes.NewBuffer([]byte{})
w := describe.NewPrefixWriter(buf)
describe.DescribePodTemplate(template, w)
return buf.String(), nil
}
type DaemonSetHistoryViewer struct {
c kubernetes.Interface
}
// ViewHistory returns a revision-to-history map as the revision history of a deployment
// TODO: this should be a describer
func (h *DaemonSetHistoryViewer) ViewHistory(namespace, name string, revision int64) (string, error) {
ds, history, err := daemonSetHistory(h.c.AppsV1(), namespace, name)
if err != nil {
return "", err
}
return printHistory(history, revision, func(history *appsv1.ControllerRevision) (*corev1.PodTemplateSpec, error) {
dsOfHistory, err := applyDaemonSetHistory(ds, history)
if err != nil {
return nil, err
}
return &dsOfHistory.Spec.Template, err
})
}
// printHistory returns the podTemplate of the given revision if it is non-zero
// else returns the overall revisions
func printHistory(history []*appsv1.ControllerRevision, revision int64, getPodTemplate func(history *appsv1.ControllerRevision) (*corev1.PodTemplateSpec, error)) (string, error) {
historyInfo := make(map[int64]*appsv1.ControllerRevision)
for _, history := range history {
// TODO: for now we assume revisions don't overlap, we may need to handle it
historyInfo[history.Revision] = history
}
if len(historyInfo) == 0 {
return "No rollout history found.", nil
}
// Print details of a specific revision
if revision > 0 {
history, ok := historyInfo[revision]
if !ok {
return "", fmt.Errorf("unable to find the specified revision")
}
podTemplate, err := getPodTemplate(history)
if err != nil {
return "", fmt.Errorf("unable to parse history %s", history.Name)
}
return printTemplate(podTemplate)
}
// Print an overview of all Revisions
// Sort the revisionToChangeCause map by revision
revisions := make([]int64, 0, len(historyInfo))
for r := range historyInfo {
revisions = append(revisions, r)
}
sliceutil.SortInts64(revisions)
return tabbedString(func(out io.Writer) error {
fmt.Fprintf(out, "REVISION\tCHANGE-CAUSE\n")
for _, r := range revisions {
// Find the change-cause of revision r
changeCause := historyInfo[r].Annotations[ChangeCauseAnnotation]
if len(changeCause) == 0 {
changeCause = "<none>"
}
fmt.Fprintf(out, "%d\t%s\n", r, changeCause)
}
return nil
})
}
type StatefulSetHistoryViewer struct {
c kubernetes.Interface
}
// ViewHistory returns a list of the revision history of a statefulset
// TODO: this should be a describer
func (h *StatefulSetHistoryViewer) ViewHistory(namespace, name string, revision int64) (string, error) {
sts, history, err := statefulSetHistory(h.c.AppsV1(), namespace, name)
if err != nil {
return "", err
}
return printHistory(history, revision, func(history *appsv1.ControllerRevision) (*corev1.PodTemplateSpec, error) {
stsOfHistory, err := applyStatefulSetHistory(sts, history)
if err != nil {
return nil, err
}
return &stsOfHistory.Spec.Template, err
})
}
// controlledHistories returns all ControllerRevisions in namespace that selected by selector and owned by accessor
// TODO: Rename this to controllerHistory when other controllers have been upgraded
func controlledHistoryV1(
apps clientappsv1.AppsV1Interface,
namespace string,
selector labels.Selector,
accessor metav1.Object) ([]*appsv1.ControllerRevision, error) {
var result []*appsv1.ControllerRevision
historyList, err := apps.ControllerRevisions(namespace).List(context.TODO(), metav1.ListOptions{LabelSelector: selector.String()})
if err != nil {
return nil, err
}
for i := range historyList.Items {
history := historyList.Items[i]
// Only add history that belongs to the API object
if metav1.IsControlledBy(&history, accessor) {
result = append(result, &history)
}
}
return result, nil
}
// controlledHistories returns all ControllerRevisions in namespace that selected by selector and owned by accessor
func controlledHistory(
apps clientappsv1.AppsV1Interface,
namespace string,
selector labels.Selector,
accessor metav1.Object) ([]*appsv1.ControllerRevision, error) {
var result []*appsv1.ControllerRevision
historyList, err := apps.ControllerRevisions(namespace).List(context.TODO(), metav1.ListOptions{LabelSelector: selector.String()})
if err != nil {
return nil, err
}
for i := range historyList.Items {
history := historyList.Items[i]
// Only add history that belongs to the API object
if metav1.IsControlledBy(&history, accessor) {
result = append(result, &history)
}
}
return result, nil
}
// daemonSetHistory returns the DaemonSet named name in namespace and all ControllerRevisions in its history.
func daemonSetHistory(
apps clientappsv1.AppsV1Interface,
namespace, name string) (*appsv1.DaemonSet, []*appsv1.ControllerRevision, error) {
ds, err := apps.DaemonSets(namespace).Get(context.TODO(), name, metav1.GetOptions{})
if err != nil {
return nil, nil, fmt.Errorf("failed to retrieve DaemonSet %s: %v", name, err)
}
selector, err := metav1.LabelSelectorAsSelector(ds.Spec.Selector)
if err != nil {
return nil, nil, fmt.Errorf("failed to create selector for DaemonSet %s: %v", ds.Name, err)
}
accessor, err := meta.Accessor(ds)
if err != nil {
return nil, nil, fmt.Errorf("failed to create accessor for DaemonSet %s: %v", ds.Name, err)
}
history, err := controlledHistory(apps, ds.Namespace, selector, accessor)
if err != nil {
return nil, nil, fmt.Errorf("unable to find history controlled by DaemonSet %s: %v", ds.Name, err)
}
return ds, history, nil
}
// statefulSetHistory returns the StatefulSet named name in namespace and all ControllerRevisions in its history.
func statefulSetHistory(
apps clientappsv1.AppsV1Interface,
namespace, name string) (*appsv1.StatefulSet, []*appsv1.ControllerRevision, error) {
sts, err := apps.StatefulSets(namespace).Get(context.TODO(), name, metav1.GetOptions{})
if err != nil {
return nil, nil, fmt.Errorf("failed to retrieve Statefulset %s: %s", name, err.Error())
}
selector, err := metav1.LabelSelectorAsSelector(sts.Spec.Selector)
if err != nil {
return nil, nil, fmt.Errorf("failed to create selector for StatefulSet %s: %s", name, err.Error())
}
accessor, err := meta.Accessor(sts)
if err != nil {
return nil, nil, fmt.Errorf("failed to obtain accessor for StatefulSet %s: %s", name, err.Error())
}
history, err := controlledHistoryV1(apps, namespace, selector, accessor)
if err != nil {
return nil, nil, fmt.Errorf("unable to find history controlled by StatefulSet %s: %v", name, err)
}
return sts, history, nil
}
// applyDaemonSetHistory returns a specific revision of DaemonSet by applying the given history to a copy of the given DaemonSet
func applyDaemonSetHistory(ds *appsv1.DaemonSet, history *appsv1.ControllerRevision) (*appsv1.DaemonSet, error) {
dsBytes, err := json.Marshal(ds)
if err != nil {
return nil, err
}
patched, err := strategicpatch.StrategicMergePatch(dsBytes, history.Data.Raw, ds)
if err != nil {
return nil, err
}
result := &appsv1.DaemonSet{}
err = json.Unmarshal(patched, result)
if err != nil {
return nil, err
}
return result, nil
}
// applyStatefulSetHistory returns a specific revision of StatefulSet by applying the given history to a copy of the given StatefulSet
func applyStatefulSetHistory(sts *appsv1.StatefulSet, history *appsv1.ControllerRevision) (*appsv1.StatefulSet, error) {
stsBytes, err := json.Marshal(sts)
if err != nil {
return nil, err
}
patched, err := strategicpatch.StrategicMergePatch(stsBytes, history.Data.Raw, sts)
if err != nil {
return nil, err
}
result := &appsv1.StatefulSet{}
err = json.Unmarshal(patched, result)
if err != nil {
return nil, err
}
return result, nil
}
// TODO: copied here until this becomes a describer
func tabbedString(f func(io.Writer) error) (string, error) {
out := new(tabwriter.Writer)
buf := &bytes.Buffer{}
out.Init(buf, 0, 8, 2, ' ', 0)
err := f(out)
if err != nil {
return "", err
}
out.Flush()
str := string(buf.String())
return str, nil
}
// getChangeCause returns the change-cause annotation of the input object
func getChangeCause(obj runtime.Object) string {
accessor, err := meta.Accessor(obj)
if err != nil {
return ""
}
return accessor.GetAnnotations()[ChangeCauseAnnotation]
}

View File

@ -0,0 +1,37 @@
/*
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 polymorphichelpers
import (
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/client-go/kubernetes"
)
// historyViewer Returns a HistoryViewer for viewing change history
func historyViewer(restClientGetter genericclioptions.RESTClientGetter, mapping *meta.RESTMapping) (HistoryViewer, error) {
clientConfig, err := restClientGetter.ToRESTConfig()
if err != nil {
return nil, err
}
external, err := kubernetes.NewForConfig(clientConfig)
if err != nil {
return nil, err
}
return HistoryViewerFor(mapping.GroupVersionKind.GroupKind(), external)
}

View File

@ -0,0 +1,114 @@
/*
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 polymorphichelpers
import (
"time"
"k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/client-go/rest"
)
// LogsForObjectFunc is a function type that can tell you how to get logs for a runtime.object
type LogsForObjectFunc func(restClientGetter genericclioptions.RESTClientGetter, object, options runtime.Object, timeout time.Duration, allContainers bool) (map[v1.ObjectReference]rest.ResponseWrapper, error)
// LogsForObjectFn gives a way to easily override the function for unit testing if needed.
var LogsForObjectFn LogsForObjectFunc = logsForObject
// AttachablePodForObjectFunc is a function type that can tell you how to get the pod for which to attach a given object
type AttachablePodForObjectFunc func(restClientGetter genericclioptions.RESTClientGetter, object runtime.Object, timeout time.Duration) (*v1.Pod, error)
// AttachablePodForObjectFn gives a way to easily override the function for unit testing if needed.
var AttachablePodForObjectFn AttachablePodForObjectFunc = attachablePodForObject
// HistoryViewerFunc is a function type that can tell you how to view change history
type HistoryViewerFunc func(restClientGetter genericclioptions.RESTClientGetter, mapping *meta.RESTMapping) (HistoryViewer, error)
// HistoryViewerFn gives a way to easily override the function for unit testing if needed
var HistoryViewerFn HistoryViewerFunc = historyViewer
// StatusViewerFunc is a function type that can tell you how to print rollout status
type StatusViewerFunc func(mapping *meta.RESTMapping) (StatusViewer, error)
// StatusViewerFn gives a way to easily override the function for unit testing if needed
var StatusViewerFn StatusViewerFunc = statusViewer
// UpdatePodSpecForObjectFunc will call the provided function on the pod spec this object supports,
// return false if no pod spec is supported, or return an error.
type UpdatePodSpecForObjectFunc func(obj runtime.Object, fn func(*v1.PodSpec) error) (bool, error)
// UpdatePodSpecForObjectFn gives a way to easily override the function for unit testing if needed
var UpdatePodSpecForObjectFn UpdatePodSpecForObjectFunc = updatePodSpecForObject
// MapBasedSelectorForObjectFunc will call the provided function on mapping the baesd selector for object,
// return "" if object is not supported, or return an error.
type MapBasedSelectorForObjectFunc func(object runtime.Object) (string, error)
// MapBasedSelectorForObjectFn gives a way to easily override the function for unit testing if needed
var MapBasedSelectorForObjectFn MapBasedSelectorForObjectFunc = mapBasedSelectorForObject
// ProtocolsForObjectFunc will call the provided function on the protocols for the object,
// return nil-map if no protocols for the object, or return an error.
type ProtocolsForObjectFunc func(object runtime.Object) (map[string]string, error)
// ProtocolsForObjectFn gives a way to easily override the function for unit testing if needed
var ProtocolsForObjectFn ProtocolsForObjectFunc = protocolsForObject
// PortsForObjectFunc returns the ports associated with the provided object
type PortsForObjectFunc func(object runtime.Object) ([]string, error)
// PortsForObjectFn gives a way to easily override the function for unit testing if needed
var PortsForObjectFn PortsForObjectFunc = portsForObject
// CanBeExposedFunc is a function type that can tell you whether a given GroupKind is capable of being exposed
type CanBeExposedFunc func(kind schema.GroupKind) error
// CanBeExposedFn gives a way to easily override the function for unit testing if needed
var CanBeExposedFn CanBeExposedFunc = canBeExposed
// ObjectPauserFunc is a function type that marks the object in a given info as paused.
type ObjectPauserFunc func(runtime.Object) ([]byte, error)
// ObjectPauserFn gives a way to easily override the function for unit testing if needed.
// Returns the patched object in bytes and any error that occurred during the encoding or
// in case the object is already paused.
var ObjectPauserFn ObjectPauserFunc = defaultObjectPauser
// ObjectResumerFunc is a function type that marks the object in a given info as resumed.
type ObjectResumerFunc func(runtime.Object) ([]byte, error)
// ObjectResumerFn gives a way to easily override the function for unit testing if needed.
// Returns the patched object in bytes and any error that occurred during the encoding or
// in case the object is already resumed.
var ObjectResumerFn ObjectResumerFunc = defaultObjectResumer
// RollbackerFunc gives a way to change the rollback version of the specified RESTMapping type
type RollbackerFunc func(restClientGetter genericclioptions.RESTClientGetter, mapping *meta.RESTMapping) (Rollbacker, error)
// RollbackerFn gives a way to easily override the function for unit testing if needed
var RollbackerFn RollbackerFunc = rollbacker
// ObjectRestarterFunc is a function type that updates an annotation in a deployment to restart it..
type ObjectRestarterFunc func(runtime.Object) ([]byte, error)
// ObjectRestarterFn gives a way to easily override the function for unit testing if needed.
// Returns the patched object in bytes and any error that occurred during the encoding.
var ObjectRestarterFn ObjectRestarterFunc = defaultObjectRestarter

View File

@ -0,0 +1,210 @@
/*
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 polymorphichelpers
import (
"errors"
"fmt"
"os"
"sort"
"strings"
"time"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/cli-runtime/pkg/genericclioptions"
corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/reference"
"k8s.io/kubectl/pkg/cmd/util/podcmd"
"k8s.io/kubectl/pkg/scheme"
"k8s.io/kubectl/pkg/util/podutils"
)
// defaultLogsContainerAnnotationName is an annotation name that can be used to preselect the interesting container
// from a pod when running kubectl logs. It is deprecated and will be remove in 1.25.
const defaultLogsContainerAnnotationName = "kubectl.kubernetes.io/default-logs-container"
func logsForObject(restClientGetter genericclioptions.RESTClientGetter, object, options runtime.Object, timeout time.Duration, allContainers bool) (map[corev1.ObjectReference]rest.ResponseWrapper, error) {
clientConfig, err := restClientGetter.ToRESTConfig()
if err != nil {
return nil, err
}
clientset, err := corev1client.NewForConfig(clientConfig)
if err != nil {
return nil, err
}
return logsForObjectWithClient(clientset, object, options, timeout, allContainers)
}
// this is split for easy test-ability
func logsForObjectWithClient(clientset corev1client.CoreV1Interface, object, options runtime.Object, timeout time.Duration, allContainers bool) (map[corev1.ObjectReference]rest.ResponseWrapper, error) {
opts, ok := options.(*corev1.PodLogOptions)
if !ok {
return nil, errors.New("provided options object is not a PodLogOptions")
}
switch t := object.(type) {
case *corev1.PodList:
ret := make(map[corev1.ObjectReference]rest.ResponseWrapper)
for i := range t.Items {
currRet, err := logsForObjectWithClient(clientset, &t.Items[i], options, timeout, allContainers)
if err != nil {
return nil, err
}
for k, v := range currRet {
ret[k] = v
}
}
return ret, nil
case *corev1.Pod:
// if allContainers is true, then we're going to locate all containers and then iterate through them. At that point, "allContainers" is false
if !allContainers {
currOpts := new(corev1.PodLogOptions)
if opts != nil {
opts.DeepCopyInto(currOpts)
}
// in case the "kubectl.kubernetes.io/default-container" annotation is present, we preset the opts.Containers to default to selected
// container. This gives users ability to preselect the most interesting container in pod.
if annotations := t.GetAnnotations(); annotations != nil && currOpts.Container == "" {
var defaultContainer string
if len(annotations[podcmd.DefaultContainerAnnotationName]) > 0 {
defaultContainer = annotations[podcmd.DefaultContainerAnnotationName]
} else if len(annotations[defaultLogsContainerAnnotationName]) > 0 {
// Only log deprecation if we have only the old annotation. This allows users to
// set both to support multiple versions of kubectl; if they are setting both
// they must already know it is deprecated, so we don't need to add noisy
// warnings.
defaultContainer = annotations[defaultLogsContainerAnnotationName]
fmt.Fprintf(os.Stderr, "Using deprecated annotation `kubectl.kubernetes.io/default-logs-container` in pod/%v. Please use `kubectl.kubernetes.io/default-container` instead\n", t.Name)
}
if len(defaultContainer) > 0 {
if exists, _ := podcmd.FindContainerByName(t, defaultContainer); exists == nil {
fmt.Fprintf(os.Stderr, "Default container name %q not found in pod %s\n", defaultContainer, t.Name)
} else {
currOpts.Container = defaultContainer
}
}
}
if currOpts.Container == "" {
// We don't know container name. In this case we expect only one container to be present in the pod (ignoring InitContainers).
// If there is more than one container, we should return an error showing all container names.
if len(t.Spec.Containers) != 1 {
containerNames := getContainerNames(t.Spec.Containers)
initContainerNames := getContainerNames(t.Spec.InitContainers)
ephemeralContainerNames := getContainerNames(ephemeralContainersToContainers(t.Spec.EphemeralContainers))
err := fmt.Sprintf("a container name must be specified for pod %s, choose one of: [%s]", t.Name, containerNames)
if len(initContainerNames) > 0 {
err += fmt.Sprintf(" or one of the init containers: [%s]", initContainerNames)
}
if len(ephemeralContainerNames) > 0 {
err += fmt.Sprintf(" or one of the ephemeral containers: [%s]", ephemeralContainerNames)
}
return nil, errors.New(err)
}
currOpts.Container = t.Spec.Containers[0].Name
}
container, fieldPath := podcmd.FindContainerByName(t, currOpts.Container)
if container == nil {
return nil, fmt.Errorf("container %s is not valid for pod %s", currOpts.Container, t.Name)
}
ref, err := reference.GetPartialReference(scheme.Scheme, t, fieldPath)
if err != nil {
return nil, fmt.Errorf("Unable to construct reference to '%#v': %v", t, err)
}
ret := make(map[corev1.ObjectReference]rest.ResponseWrapper, 1)
ret[*ref] = clientset.Pods(t.Namespace).GetLogs(t.Name, currOpts)
return ret, nil
}
ret := make(map[corev1.ObjectReference]rest.ResponseWrapper)
for _, c := range t.Spec.InitContainers {
currOpts := opts.DeepCopy()
currOpts.Container = c.Name
currRet, err := logsForObjectWithClient(clientset, t, currOpts, timeout, false)
if err != nil {
return nil, err
}
for k, v := range currRet {
ret[k] = v
}
}
for _, c := range t.Spec.Containers {
currOpts := opts.DeepCopy()
currOpts.Container = c.Name
currRet, err := logsForObjectWithClient(clientset, t, currOpts, timeout, false)
if err != nil {
return nil, err
}
for k, v := range currRet {
ret[k] = v
}
}
for _, c := range t.Spec.EphemeralContainers {
currOpts := opts.DeepCopy()
currOpts.Container = c.Name
currRet, err := logsForObjectWithClient(clientset, t, currOpts, timeout, false)
if err != nil {
return nil, err
}
for k, v := range currRet {
ret[k] = v
}
}
return ret, nil
}
namespace, selector, err := SelectorsForObject(object)
if err != nil {
return nil, fmt.Errorf("cannot get the logs from %T: %v", object, err)
}
sortBy := func(pods []*corev1.Pod) sort.Interface { return podutils.ByLogging(pods) }
pod, numPods, err := GetFirstPod(clientset, namespace, selector.String(), timeout, sortBy)
if err != nil {
return nil, err
}
if numPods > 1 {
fmt.Fprintf(os.Stderr, "Found %v pods, using pod/%v\n", numPods, pod.Name)
}
return logsForObjectWithClient(clientset, pod, options, timeout, allContainers)
}
// getContainerNames returns a formatted string containing the container names
func getContainerNames(containers []corev1.Container) string {
names := []string{}
for _, c := range containers {
names = append(names, c.Name)
}
return strings.Join(names, " ")
}
func ephemeralContainersToContainers(containers []corev1.EphemeralContainer) []corev1.Container {
var ec []corev1.Container
for i := range containers {
ec = append(ec, corev1.Container(containers[i].EphemeralContainerCommon))
}
return ec
}

View File

@ -0,0 +1,160 @@
/*
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 polymorphichelpers
import (
"fmt"
"strings"
appsv1 "k8s.io/api/apps/v1"
appsv1beta1 "k8s.io/api/apps/v1beta1"
appsv1beta2 "k8s.io/api/apps/v1beta2"
corev1 "k8s.io/api/core/v1"
extensionsv1beta1 "k8s.io/api/extensions/v1beta1"
"k8s.io/apimachinery/pkg/runtime"
)
// mapBasedSelectorForObject returns the map-based selector associated with the provided object. If a
// new set-based selector is provided, an error is returned if the selector cannot be converted to a
// map-based selector
func mapBasedSelectorForObject(object runtime.Object) (string, error) {
// TODO: replace with a swagger schema based approach (identify pod selector via schema introspection)
switch t := object.(type) {
case *corev1.ReplicationController:
return MakeLabels(t.Spec.Selector), nil
case *corev1.Pod:
if len(t.Labels) == 0 {
return "", fmt.Errorf("the pod has no labels and cannot be exposed")
}
return MakeLabels(t.Labels), nil
case *corev1.Service:
if t.Spec.Selector == nil {
return "", fmt.Errorf("the service has no pod selector set")
}
return MakeLabels(t.Spec.Selector), nil
case *extensionsv1beta1.Deployment:
// "extensions" deployments use pod template labels if selector is not set.
var labels map[string]string
if t.Spec.Selector != nil {
// TODO(madhusudancs): Make this smarter by admitting MatchExpressions with Equals
// operator, DoubleEquals operator and In operator with only one element in the set.
if len(t.Spec.Selector.MatchExpressions) > 0 {
return "", fmt.Errorf("couldn't convert expressions - \"%+v\" to map-based selector format", t.Spec.Selector.MatchExpressions)
}
labels = t.Spec.Selector.MatchLabels
} else {
labels = t.Spec.Template.Labels
}
if len(labels) == 0 {
return "", fmt.Errorf("the deployment has no labels or selectors and cannot be exposed")
}
return MakeLabels(labels), nil
case *appsv1.Deployment:
// "apps" deployments must have the selector set.
if t.Spec.Selector == nil || len(t.Spec.Selector.MatchLabels) == 0 {
return "", fmt.Errorf("invalid deployment: no selectors, therefore cannot be exposed")
}
// TODO(madhusudancs): Make this smarter by admitting MatchExpressions with Equals
// operator, DoubleEquals operator and In operator with only one element in the set.
if len(t.Spec.Selector.MatchExpressions) > 0 {
return "", fmt.Errorf("couldn't convert expressions - \"%+v\" to map-based selector format", t.Spec.Selector.MatchExpressions)
}
return MakeLabels(t.Spec.Selector.MatchLabels), nil
case *appsv1beta2.Deployment:
// "apps" deployments must have the selector set.
if t.Spec.Selector == nil || len(t.Spec.Selector.MatchLabels) == 0 {
return "", fmt.Errorf("invalid deployment: no selectors, therefore cannot be exposed")
}
// TODO(madhusudancs): Make this smarter by admitting MatchExpressions with Equals
// operator, DoubleEquals operator and In operator with only one element in the set.
if len(t.Spec.Selector.MatchExpressions) > 0 {
return "", fmt.Errorf("couldn't convert expressions - \"%+v\" to map-based selector format", t.Spec.Selector.MatchExpressions)
}
return MakeLabels(t.Spec.Selector.MatchLabels), nil
case *appsv1beta1.Deployment:
// "apps" deployments must have the selector set.
if t.Spec.Selector == nil || len(t.Spec.Selector.MatchLabels) == 0 {
return "", fmt.Errorf("invalid deployment: no selectors, therefore cannot be exposed")
}
// TODO(madhusudancs): Make this smarter by admitting MatchExpressions with Equals
// operator, DoubleEquals operator and In operator with only one element in the set.
if len(t.Spec.Selector.MatchExpressions) > 0 {
return "", fmt.Errorf("couldn't convert expressions - \"%+v\" to map-based selector format", t.Spec.Selector.MatchExpressions)
}
return MakeLabels(t.Spec.Selector.MatchLabels), nil
case *extensionsv1beta1.ReplicaSet:
// "extensions" replicasets use pod template labels if selector is not set.
var labels map[string]string
if t.Spec.Selector != nil {
// TODO(madhusudancs): Make this smarter by admitting MatchExpressions with Equals
// operator, DoubleEquals operator and In operator with only one element in the set.
if len(t.Spec.Selector.MatchExpressions) > 0 {
return "", fmt.Errorf("couldn't convert expressions - \"%+v\" to map-based selector format", t.Spec.Selector.MatchExpressions)
}
labels = t.Spec.Selector.MatchLabels
} else {
labels = t.Spec.Template.Labels
}
if len(labels) == 0 {
return "", fmt.Errorf("the replica set has no labels or selectors and cannot be exposed")
}
return MakeLabels(labels), nil
case *appsv1.ReplicaSet:
// "apps" replicasets must have the selector set.
if t.Spec.Selector == nil || len(t.Spec.Selector.MatchLabels) == 0 {
return "", fmt.Errorf("invalid replicaset: no selectors, therefore cannot be exposed")
}
// TODO(madhusudancs): Make this smarter by admitting MatchExpressions with Equals
// operator, DoubleEquals operator and In operator with only one element in the set.
if len(t.Spec.Selector.MatchExpressions) > 0 {
return "", fmt.Errorf("couldn't convert expressions - \"%+v\" to map-based selector format", t.Spec.Selector.MatchExpressions)
}
return MakeLabels(t.Spec.Selector.MatchLabels), nil
case *appsv1beta2.ReplicaSet:
// "apps" replicasets must have the selector set.
if t.Spec.Selector == nil || len(t.Spec.Selector.MatchLabels) == 0 {
return "", fmt.Errorf("invalid replicaset: no selectors, therefore cannot be exposed")
}
// TODO(madhusudancs): Make this smarter by admitting MatchExpressions with Equals
// operator, DoubleEquals operator and In operator with only one element in the set.
if len(t.Spec.Selector.MatchExpressions) > 0 {
return "", fmt.Errorf("couldn't convert expressions - \"%+v\" to map-based selector format", t.Spec.Selector.MatchExpressions)
}
return MakeLabels(t.Spec.Selector.MatchLabels), nil
default:
return "", fmt.Errorf("cannot extract pod selector from %T", object)
}
}
func MakeLabels(labels map[string]string) string {
out := []string{}
for key, value := range labels {
out = append(out, fmt.Sprintf("%s=%s", key, value))
}
return strings.Join(out, ",")
}

View File

@ -0,0 +1,65 @@
/*
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 polymorphichelpers
import (
"errors"
"fmt"
appsv1 "k8s.io/api/apps/v1"
appsv1beta1 "k8s.io/api/apps/v1beta1"
appsv1beta2 "k8s.io/api/apps/v1beta2"
extensionsv1beta1 "k8s.io/api/extensions/v1beta1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/kubectl/pkg/scheme"
)
// Currently only supports Deployments.
func defaultObjectPauser(obj runtime.Object) ([]byte, error) {
switch obj := obj.(type) {
case *extensionsv1beta1.Deployment:
if obj.Spec.Paused {
return nil, errors.New("is already paused")
}
obj.Spec.Paused = true
return runtime.Encode(scheme.Codecs.LegacyCodec(extensionsv1beta1.SchemeGroupVersion), obj)
case *appsv1.Deployment:
if obj.Spec.Paused {
return nil, errors.New("is already paused")
}
obj.Spec.Paused = true
return runtime.Encode(scheme.Codecs.LegacyCodec(appsv1.SchemeGroupVersion), obj)
case *appsv1beta2.Deployment:
if obj.Spec.Paused {
return nil, errors.New("is already paused")
}
obj.Spec.Paused = true
return runtime.Encode(scheme.Codecs.LegacyCodec(appsv1beta2.SchemeGroupVersion), obj)
case *appsv1beta1.Deployment:
if obj.Spec.Paused {
return nil, errors.New("is already paused")
}
obj.Spec.Paused = true
return runtime.Encode(scheme.Codecs.LegacyCodec(appsv1beta1.SchemeGroupVersion), obj)
default:
return nil, fmt.Errorf("pausing is not supported")
}
}

View File

@ -0,0 +1,119 @@
/*
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 polymorphichelpers
import (
"errors"
"fmt"
"time"
appsv1 "k8s.io/api/apps/v1"
appsv1beta1 "k8s.io/api/apps/v1beta1"
appsv1beta2 "k8s.io/api/apps/v1beta2"
extensionsv1beta1 "k8s.io/api/extensions/v1beta1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/kubectl/pkg/scheme"
)
func defaultObjectRestarter(obj runtime.Object) ([]byte, error) {
switch obj := obj.(type) {
case *extensionsv1beta1.Deployment:
if obj.Spec.Paused {
return nil, errors.New("can't restart paused deployment (run rollout resume first)")
}
if obj.Spec.Template.ObjectMeta.Annotations == nil {
obj.Spec.Template.ObjectMeta.Annotations = make(map[string]string)
}
obj.Spec.Template.ObjectMeta.Annotations["kubectl.kubernetes.io/restartedAt"] = time.Now().Format(time.RFC3339)
return runtime.Encode(scheme.Codecs.LegacyCodec(extensionsv1beta1.SchemeGroupVersion), obj)
case *appsv1.Deployment:
if obj.Spec.Paused {
return nil, errors.New("can't restart paused deployment (run rollout resume first)")
}
if obj.Spec.Template.ObjectMeta.Annotations == nil {
obj.Spec.Template.ObjectMeta.Annotations = make(map[string]string)
}
obj.Spec.Template.ObjectMeta.Annotations["kubectl.kubernetes.io/restartedAt"] = time.Now().Format(time.RFC3339)
return runtime.Encode(scheme.Codecs.LegacyCodec(appsv1.SchemeGroupVersion), obj)
case *appsv1beta2.Deployment:
if obj.Spec.Paused {
return nil, errors.New("can't restart paused deployment (run rollout resume first)")
}
if obj.Spec.Template.ObjectMeta.Annotations == nil {
obj.Spec.Template.ObjectMeta.Annotations = make(map[string]string)
}
obj.Spec.Template.ObjectMeta.Annotations["kubectl.kubernetes.io/restartedAt"] = time.Now().Format(time.RFC3339)
return runtime.Encode(scheme.Codecs.LegacyCodec(appsv1beta2.SchemeGroupVersion), obj)
case *appsv1beta1.Deployment:
if obj.Spec.Paused {
return nil, errors.New("can't restart paused deployment (run rollout resume first)")
}
if obj.Spec.Template.ObjectMeta.Annotations == nil {
obj.Spec.Template.ObjectMeta.Annotations = make(map[string]string)
}
obj.Spec.Template.ObjectMeta.Annotations["kubectl.kubernetes.io/restartedAt"] = time.Now().Format(time.RFC3339)
return runtime.Encode(scheme.Codecs.LegacyCodec(appsv1beta1.SchemeGroupVersion), obj)
case *extensionsv1beta1.DaemonSet:
if obj.Spec.Template.ObjectMeta.Annotations == nil {
obj.Spec.Template.ObjectMeta.Annotations = make(map[string]string)
}
obj.Spec.Template.ObjectMeta.Annotations["kubectl.kubernetes.io/restartedAt"] = time.Now().Format(time.RFC3339)
return runtime.Encode(scheme.Codecs.LegacyCodec(extensionsv1beta1.SchemeGroupVersion), obj)
case *appsv1.DaemonSet:
if obj.Spec.Template.ObjectMeta.Annotations == nil {
obj.Spec.Template.ObjectMeta.Annotations = make(map[string]string)
}
obj.Spec.Template.ObjectMeta.Annotations["kubectl.kubernetes.io/restartedAt"] = time.Now().Format(time.RFC3339)
return runtime.Encode(scheme.Codecs.LegacyCodec(appsv1.SchemeGroupVersion), obj)
case *appsv1beta2.DaemonSet:
if obj.Spec.Template.ObjectMeta.Annotations == nil {
obj.Spec.Template.ObjectMeta.Annotations = make(map[string]string)
}
obj.Spec.Template.ObjectMeta.Annotations["kubectl.kubernetes.io/restartedAt"] = time.Now().Format(time.RFC3339)
return runtime.Encode(scheme.Codecs.LegacyCodec(appsv1beta2.SchemeGroupVersion), obj)
case *appsv1.StatefulSet:
if obj.Spec.Template.ObjectMeta.Annotations == nil {
obj.Spec.Template.ObjectMeta.Annotations = make(map[string]string)
}
obj.Spec.Template.ObjectMeta.Annotations["kubectl.kubernetes.io/restartedAt"] = time.Now().Format(time.RFC3339)
return runtime.Encode(scheme.Codecs.LegacyCodec(appsv1.SchemeGroupVersion), obj)
case *appsv1beta1.StatefulSet:
if obj.Spec.Template.ObjectMeta.Annotations == nil {
obj.Spec.Template.ObjectMeta.Annotations = make(map[string]string)
}
obj.Spec.Template.ObjectMeta.Annotations["kubectl.kubernetes.io/restartedAt"] = time.Now().Format(time.RFC3339)
return runtime.Encode(scheme.Codecs.LegacyCodec(appsv1beta1.SchemeGroupVersion), obj)
case *appsv1beta2.StatefulSet:
if obj.Spec.Template.ObjectMeta.Annotations == nil {
obj.Spec.Template.ObjectMeta.Annotations = make(map[string]string)
}
obj.Spec.Template.ObjectMeta.Annotations["kubectl.kubernetes.io/restartedAt"] = time.Now().Format(time.RFC3339)
return runtime.Encode(scheme.Codecs.LegacyCodec(appsv1beta2.SchemeGroupVersion), obj)
default:
return nil, fmt.Errorf("restarting is not supported")
}
}

View File

@ -0,0 +1,64 @@
/*
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 polymorphichelpers
import (
"errors"
"fmt"
appsv1 "k8s.io/api/apps/v1"
appsv1beta1 "k8s.io/api/apps/v1beta1"
appsv1beta2 "k8s.io/api/apps/v1beta2"
extensionsv1beta1 "k8s.io/api/extensions/v1beta1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/kubectl/pkg/scheme"
)
func defaultObjectResumer(obj runtime.Object) ([]byte, error) {
switch obj := obj.(type) {
case *extensionsv1beta1.Deployment:
if !obj.Spec.Paused {
return nil, errors.New("is not paused")
}
obj.Spec.Paused = false
return runtime.Encode(scheme.Codecs.LegacyCodec(extensionsv1beta1.SchemeGroupVersion), obj)
case *appsv1.Deployment:
if !obj.Spec.Paused {
return nil, errors.New("is not paused")
}
obj.Spec.Paused = false
return runtime.Encode(scheme.Codecs.LegacyCodec(appsv1.SchemeGroupVersion), obj)
case *appsv1beta2.Deployment:
if !obj.Spec.Paused {
return nil, errors.New("is not paused")
}
obj.Spec.Paused = false
return runtime.Encode(scheme.Codecs.LegacyCodec(appsv1beta2.SchemeGroupVersion), obj)
case *appsv1beta1.Deployment:
if !obj.Spec.Paused {
return nil, errors.New("is not paused")
}
obj.Spec.Paused = false
return runtime.Encode(scheme.Codecs.LegacyCodec(appsv1beta1.SchemeGroupVersion), obj)
default:
return nil, fmt.Errorf("resuming is not supported")
}
}

View File

@ -0,0 +1,78 @@
/*
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 polymorphichelpers
import (
"fmt"
"strconv"
appsv1 "k8s.io/api/apps/v1"
appsv1beta1 "k8s.io/api/apps/v1beta1"
appsv1beta2 "k8s.io/api/apps/v1beta2"
corev1 "k8s.io/api/core/v1"
extensionsv1beta1 "k8s.io/api/extensions/v1beta1"
"k8s.io/apimachinery/pkg/runtime"
)
func portsForObject(object runtime.Object) ([]string, error) {
switch t := object.(type) {
case *corev1.ReplicationController:
return getPorts(t.Spec.Template.Spec), nil
case *corev1.Pod:
return getPorts(t.Spec), nil
case *corev1.Service:
return getServicePorts(t.Spec), nil
case *extensionsv1beta1.Deployment:
return getPorts(t.Spec.Template.Spec), nil
case *appsv1.Deployment:
return getPorts(t.Spec.Template.Spec), nil
case *appsv1beta2.Deployment:
return getPorts(t.Spec.Template.Spec), nil
case *appsv1beta1.Deployment:
return getPorts(t.Spec.Template.Spec), nil
case *extensionsv1beta1.ReplicaSet:
return getPorts(t.Spec.Template.Spec), nil
case *appsv1.ReplicaSet:
return getPorts(t.Spec.Template.Spec), nil
case *appsv1beta2.ReplicaSet:
return getPorts(t.Spec.Template.Spec), nil
default:
return nil, fmt.Errorf("cannot extract ports from %T", object)
}
}
func getPorts(spec corev1.PodSpec) []string {
result := []string{}
for _, container := range spec.Containers {
for _, port := range container.Ports {
result = append(result, strconv.Itoa(int(port.ContainerPort)))
}
}
return result
}
func getServicePorts(spec corev1.ServiceSpec) []string {
result := []string{}
for _, servicePort := range spec.Ports {
result = append(result, strconv.Itoa(int(servicePort.Port)))
}
return result
}

View File

@ -0,0 +1,89 @@
/*
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 polymorphichelpers
import (
"fmt"
"strconv"
appsv1 "k8s.io/api/apps/v1"
appsv1beta1 "k8s.io/api/apps/v1beta1"
appsv1beta2 "k8s.io/api/apps/v1beta2"
corev1 "k8s.io/api/core/v1"
extensionsv1beta1 "k8s.io/api/extensions/v1beta1"
"k8s.io/apimachinery/pkg/runtime"
)
func protocolsForObject(object runtime.Object) (map[string]string, error) {
// TODO: replace with a swagger schema based approach (identify pod selector via schema introspection)
switch t := object.(type) {
case *corev1.ReplicationController:
return getProtocols(t.Spec.Template.Spec), nil
case *corev1.Pod:
return getProtocols(t.Spec), nil
case *corev1.Service:
return getServiceProtocols(t.Spec), nil
case *extensionsv1beta1.Deployment:
return getProtocols(t.Spec.Template.Spec), nil
case *appsv1.Deployment:
return getProtocols(t.Spec.Template.Spec), nil
case *appsv1beta2.Deployment:
return getProtocols(t.Spec.Template.Spec), nil
case *appsv1beta1.Deployment:
return getProtocols(t.Spec.Template.Spec), nil
case *extensionsv1beta1.ReplicaSet:
return getProtocols(t.Spec.Template.Spec), nil
case *appsv1.ReplicaSet:
return getProtocols(t.Spec.Template.Spec), nil
case *appsv1beta2.ReplicaSet:
return getProtocols(t.Spec.Template.Spec), nil
default:
return nil, fmt.Errorf("cannot extract protocols from %T", object)
}
}
func getProtocols(spec corev1.PodSpec) map[string]string {
result := make(map[string]string)
for _, container := range spec.Containers {
for _, port := range container.Ports {
// Empty protocol must be defaulted (TCP)
if len(port.Protocol) == 0 {
port.Protocol = corev1.ProtocolTCP
}
result[strconv.Itoa(int(port.ContainerPort))] = string(port.Protocol)
}
}
return result
}
// Extracts the protocols exposed by a service from the given service spec.
func getServiceProtocols(spec corev1.ServiceSpec) map[string]string {
result := make(map[string]string)
for _, servicePort := range spec.Ports {
// Empty protocol must be defaulted (TCP)
if len(servicePort.Protocol) == 0 {
servicePort.Protocol = corev1.ProtocolTCP
}
result[strconv.Itoa(int(servicePort.Port))] = string(servicePort.Protocol)
}
return result
}

View File

@ -0,0 +1,500 @@
/*
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 polymorphichelpers
import (
"bytes"
"context"
"fmt"
"sort"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
apiequality "k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/json"
"k8s.io/apimachinery/pkg/util/strategicpatch"
"k8s.io/client-go/kubernetes"
"k8s.io/kubectl/pkg/apps"
cmdutil "k8s.io/kubectl/pkg/cmd/util"
"k8s.io/kubectl/pkg/scheme"
deploymentutil "k8s.io/kubectl/pkg/util/deployment"
)
const (
rollbackSuccess = "rolled back"
rollbackSkipped = "skipped rollback"
)
// Rollbacker provides an interface for resources that can be rolled back.
type Rollbacker interface {
Rollback(obj runtime.Object, updatedAnnotations map[string]string, toRevision int64, dryRunStrategy cmdutil.DryRunStrategy) (string, error)
}
type RollbackVisitor struct {
clientset kubernetes.Interface
result Rollbacker
}
func (v *RollbackVisitor) VisitDeployment(elem apps.GroupKindElement) {
v.result = &DeploymentRollbacker{v.clientset}
}
func (v *RollbackVisitor) VisitStatefulSet(kind apps.GroupKindElement) {
v.result = &StatefulSetRollbacker{v.clientset}
}
func (v *RollbackVisitor) VisitDaemonSet(kind apps.GroupKindElement) {
v.result = &DaemonSetRollbacker{v.clientset}
}
func (v *RollbackVisitor) VisitJob(kind apps.GroupKindElement) {}
func (v *RollbackVisitor) VisitPod(kind apps.GroupKindElement) {}
func (v *RollbackVisitor) VisitReplicaSet(kind apps.GroupKindElement) {}
func (v *RollbackVisitor) VisitReplicationController(kind apps.GroupKindElement) {}
func (v *RollbackVisitor) VisitCronJob(kind apps.GroupKindElement) {}
// RollbackerFor returns an implementation of Rollbacker interface for the given schema kind
func RollbackerFor(kind schema.GroupKind, c kubernetes.Interface) (Rollbacker, error) {
elem := apps.GroupKindElement(kind)
visitor := &RollbackVisitor{
clientset: c,
}
err := elem.Accept(visitor)
if err != nil {
return nil, fmt.Errorf("error retrieving rollbacker for %q, %v", kind.String(), err)
}
if visitor.result == nil {
return nil, fmt.Errorf("no rollbacker has been implemented for %q", kind)
}
return visitor.result, nil
}
type DeploymentRollbacker struct {
c kubernetes.Interface
}
func (r *DeploymentRollbacker) Rollback(obj runtime.Object, updatedAnnotations map[string]string, toRevision int64, dryRunStrategy cmdutil.DryRunStrategy) (string, error) {
if toRevision < 0 {
return "", revisionNotFoundErr(toRevision)
}
accessor, err := meta.Accessor(obj)
if err != nil {
return "", fmt.Errorf("failed to create accessor for kind %v: %s", obj.GetObjectKind(), err.Error())
}
name := accessor.GetName()
namespace := accessor.GetNamespace()
// TODO: Fix this after kubectl has been removed from core. It is not possible to convert the runtime.Object
// to the external appsv1 Deployment without round-tripping through an internal version of Deployment. We're
// currently getting rid of all internal versions of resources. So we specifically request the appsv1 version
// here. This follows the same pattern as for DaemonSet and StatefulSet.
deployment, err := r.c.AppsV1().Deployments(namespace).Get(context.TODO(), name, metav1.GetOptions{})
if err != nil {
return "", fmt.Errorf("failed to retrieve Deployment %s: %v", name, err)
}
rsForRevision, err := deploymentRevision(deployment, r.c, toRevision)
if err != nil {
return "", err
}
if dryRunStrategy == cmdutil.DryRunClient {
return printTemplate(&rsForRevision.Spec.Template)
}
if deployment.Spec.Paused {
return "", fmt.Errorf("you cannot rollback a paused deployment; resume it first with 'kubectl rollout resume deployment/%s' and try again", name)
}
// Skip if the revision already matches current Deployment
if equalIgnoreHash(&rsForRevision.Spec.Template, &deployment.Spec.Template) {
return fmt.Sprintf("%s (current template already matches revision %d)", rollbackSkipped, toRevision), nil
}
// remove hash label before patching back into the deployment
delete(rsForRevision.Spec.Template.Labels, appsv1.DefaultDeploymentUniqueLabelKey)
// compute deployment annotations
annotations := map[string]string{}
for k := range annotationsToSkip {
if v, ok := deployment.Annotations[k]; ok {
annotations[k] = v
}
}
for k, v := range rsForRevision.Annotations {
if !annotationsToSkip[k] {
annotations[k] = v
}
}
// make patch to restore
patchType, patch, err := getDeploymentPatch(&rsForRevision.Spec.Template, annotations)
if err != nil {
return "", fmt.Errorf("failed restoring revision %d: %v", toRevision, err)
}
patchOptions := metav1.PatchOptions{}
if dryRunStrategy == cmdutil.DryRunServer {
patchOptions.DryRun = []string{metav1.DryRunAll}
}
// Restore revision
if _, err = r.c.AppsV1().Deployments(namespace).Patch(context.TODO(), name, patchType, patch, patchOptions); err != nil {
return "", fmt.Errorf("failed restoring revision %d: %v", toRevision, err)
}
return rollbackSuccess, nil
}
// equalIgnoreHash returns true if two given podTemplateSpec are equal, ignoring the diff in value of Labels[pod-template-hash]
// We ignore pod-template-hash because:
// 1. The hash result would be different upon podTemplateSpec API changes
// (e.g. the addition of a new field will cause the hash code to change)
// 2. The deployment template won't have hash labels
func equalIgnoreHash(template1, template2 *corev1.PodTemplateSpec) bool {
t1Copy := template1.DeepCopy()
t2Copy := template2.DeepCopy()
// Remove hash labels from template.Labels before comparing
delete(t1Copy.Labels, appsv1.DefaultDeploymentUniqueLabelKey)
delete(t2Copy.Labels, appsv1.DefaultDeploymentUniqueLabelKey)
return apiequality.Semantic.DeepEqual(t1Copy, t2Copy)
}
// annotationsToSkip lists the annotations that should be preserved from the deployment and not
// copied from the replicaset when rolling a deployment back
var annotationsToSkip = map[string]bool{
corev1.LastAppliedConfigAnnotation: true,
deploymentutil.RevisionAnnotation: true,
deploymentutil.RevisionHistoryAnnotation: true,
deploymentutil.DesiredReplicasAnnotation: true,
deploymentutil.MaxReplicasAnnotation: true,
appsv1.DeprecatedRollbackTo: true,
}
// getPatch returns a patch that can be applied to restore a Deployment to a
// previous version. If the returned error is nil the patch is valid.
func getDeploymentPatch(podTemplate *corev1.PodTemplateSpec, annotations map[string]string) (types.PatchType, []byte, error) {
// Create a patch of the Deployment that replaces spec.template
patch, err := json.Marshal([]interface{}{
map[string]interface{}{
"op": "replace",
"path": "/spec/template",
"value": podTemplate,
},
map[string]interface{}{
"op": "replace",
"path": "/metadata/annotations",
"value": annotations,
},
})
return types.JSONPatchType, patch, err
}
func deploymentRevision(deployment *appsv1.Deployment, c kubernetes.Interface, toRevision int64) (revision *appsv1.ReplicaSet, err error) {
_, allOldRSs, newRS, err := deploymentutil.GetAllReplicaSets(deployment, c.AppsV1())
if err != nil {
return nil, fmt.Errorf("failed to retrieve replica sets from deployment %s: %v", deployment.Name, err)
}
allRSs := allOldRSs
if newRS != nil {
allRSs = append(allRSs, newRS)
}
var (
latestReplicaSet *appsv1.ReplicaSet
latestRevision = int64(-1)
previousReplicaSet *appsv1.ReplicaSet
previousRevision = int64(-1)
)
for _, rs := range allRSs {
if v, err := deploymentutil.Revision(rs); err == nil {
if toRevision == 0 {
if latestRevision < v {
// newest one we've seen so far
previousRevision = latestRevision
previousReplicaSet = latestReplicaSet
latestRevision = v
latestReplicaSet = rs
} else if previousRevision < v {
// second newest one we've seen so far
previousRevision = v
previousReplicaSet = rs
}
} else if toRevision == v {
return rs, nil
}
}
}
if toRevision > 0 {
return nil, revisionNotFoundErr(toRevision)
}
if previousReplicaSet == nil {
return nil, fmt.Errorf("no rollout history found for deployment %q", deployment.Name)
}
return previousReplicaSet, nil
}
type DaemonSetRollbacker struct {
c kubernetes.Interface
}
func (r *DaemonSetRollbacker) Rollback(obj runtime.Object, updatedAnnotations map[string]string, toRevision int64, dryRunStrategy cmdutil.DryRunStrategy) (string, error) {
if toRevision < 0 {
return "", revisionNotFoundErr(toRevision)
}
accessor, err := meta.Accessor(obj)
if err != nil {
return "", fmt.Errorf("failed to create accessor for kind %v: %s", obj.GetObjectKind(), err.Error())
}
ds, history, err := daemonSetHistory(r.c.AppsV1(), accessor.GetNamespace(), accessor.GetName())
if err != nil {
return "", err
}
if toRevision == 0 && len(history) <= 1 {
return "", fmt.Errorf("no last revision to roll back to")
}
toHistory := findHistory(toRevision, history)
if toHistory == nil {
return "", revisionNotFoundErr(toRevision)
}
if dryRunStrategy == cmdutil.DryRunClient {
appliedDS, err := applyDaemonSetHistory(ds, toHistory)
if err != nil {
return "", err
}
return printPodTemplate(&appliedDS.Spec.Template)
}
// Skip if the revision already matches current DaemonSet
done, err := daemonSetMatch(ds, toHistory)
if err != nil {
return "", err
}
if done {
return fmt.Sprintf("%s (current template already matches revision %d)", rollbackSkipped, toRevision), nil
}
patchOptions := metav1.PatchOptions{}
if dryRunStrategy == cmdutil.DryRunServer {
patchOptions.DryRun = []string{metav1.DryRunAll}
}
// Restore revision
if _, err = r.c.AppsV1().DaemonSets(accessor.GetNamespace()).Patch(context.TODO(), accessor.GetName(), types.StrategicMergePatchType, toHistory.Data.Raw, patchOptions); err != nil {
return "", fmt.Errorf("failed restoring revision %d: %v", toRevision, err)
}
return rollbackSuccess, nil
}
// daemonMatch check if the given DaemonSet's template matches the template stored in the given history.
func daemonSetMatch(ds *appsv1.DaemonSet, history *appsv1.ControllerRevision) (bool, error) {
patch, err := getDaemonSetPatch(ds)
if err != nil {
return false, err
}
return bytes.Equal(patch, history.Data.Raw), nil
}
// getPatch returns a strategic merge patch that can be applied to restore a Daemonset to a
// previous version. If the returned error is nil the patch is valid. The current state that we save is just the
// PodSpecTemplate. We can modify this later to encompass more state (or less) and remain compatible with previously
// recorded patches.
func getDaemonSetPatch(ds *appsv1.DaemonSet) ([]byte, error) {
dsBytes, err := json.Marshal(ds)
if err != nil {
return nil, err
}
var raw map[string]interface{}
err = json.Unmarshal(dsBytes, &raw)
if err != nil {
return nil, err
}
objCopy := make(map[string]interface{})
specCopy := make(map[string]interface{})
// Create a patch of the DaemonSet that replaces spec.template
spec := raw["spec"].(map[string]interface{})
template := spec["template"].(map[string]interface{})
specCopy["template"] = template
template["$patch"] = "replace"
objCopy["spec"] = specCopy
patch, err := json.Marshal(objCopy)
return patch, err
}
type StatefulSetRollbacker struct {
c kubernetes.Interface
}
// toRevision is a non-negative integer, with 0 being reserved to indicate rolling back to previous configuration
func (r *StatefulSetRollbacker) Rollback(obj runtime.Object, updatedAnnotations map[string]string, toRevision int64, dryRunStrategy cmdutil.DryRunStrategy) (string, error) {
if toRevision < 0 {
return "", revisionNotFoundErr(toRevision)
}
accessor, err := meta.Accessor(obj)
if err != nil {
return "", fmt.Errorf("failed to create accessor for kind %v: %s", obj.GetObjectKind(), err.Error())
}
sts, history, err := statefulSetHistory(r.c.AppsV1(), accessor.GetNamespace(), accessor.GetName())
if err != nil {
return "", err
}
if toRevision == 0 && len(history) <= 1 {
return "", fmt.Errorf("no last revision to roll back to")
}
toHistory := findHistory(toRevision, history)
if toHistory == nil {
return "", revisionNotFoundErr(toRevision)
}
if dryRunStrategy == cmdutil.DryRunClient {
appliedSS, err := applyRevision(sts, toHistory)
if err != nil {
return "", err
}
return printPodTemplate(&appliedSS.Spec.Template)
}
// Skip if the revision already matches current StatefulSet
done, err := statefulsetMatch(sts, toHistory)
if err != nil {
return "", err
}
if done {
return fmt.Sprintf("%s (current template already matches revision %d)", rollbackSkipped, toRevision), nil
}
patchOptions := metav1.PatchOptions{}
if dryRunStrategy == cmdutil.DryRunServer {
patchOptions.DryRun = []string{metav1.DryRunAll}
}
// Restore revision
if _, err = r.c.AppsV1().StatefulSets(sts.Namespace).Patch(context.TODO(), sts.Name, types.StrategicMergePatchType, toHistory.Data.Raw, patchOptions); err != nil {
return "", fmt.Errorf("failed restoring revision %d: %v", toRevision, err)
}
return rollbackSuccess, nil
}
var appsCodec = scheme.Codecs.LegacyCodec(appsv1.SchemeGroupVersion)
// applyRevision returns a new StatefulSet constructed by restoring the state in revision to set. If the returned error
// is nil, the returned StatefulSet is valid.
func applyRevision(set *appsv1.StatefulSet, revision *appsv1.ControllerRevision) (*appsv1.StatefulSet, error) {
patched, err := strategicpatch.StrategicMergePatch([]byte(runtime.EncodeOrDie(appsCodec, set)), revision.Data.Raw, set)
if err != nil {
return nil, err
}
result := &appsv1.StatefulSet{}
err = json.Unmarshal(patched, result)
if err != nil {
return nil, err
}
return result, nil
}
// statefulsetMatch check if the given StatefulSet's template matches the template stored in the given history.
func statefulsetMatch(ss *appsv1.StatefulSet, history *appsv1.ControllerRevision) (bool, error) {
patch, err := getStatefulSetPatch(ss)
if err != nil {
return false, err
}
return bytes.Equal(patch, history.Data.Raw), nil
}
// getStatefulSetPatch returns a strategic merge patch that can be applied to restore a StatefulSet to a
// previous version. If the returned error is nil the patch is valid. The current state that we save is just the
// PodSpecTemplate. We can modify this later to encompass more state (or less) and remain compatible with previously
// recorded patches.
func getStatefulSetPatch(set *appsv1.StatefulSet) ([]byte, error) {
str, err := runtime.Encode(appsCodec, set)
if err != nil {
return nil, err
}
var raw map[string]interface{}
if err := json.Unmarshal([]byte(str), &raw); err != nil {
return nil, err
}
objCopy := make(map[string]interface{})
specCopy := make(map[string]interface{})
spec := raw["spec"].(map[string]interface{})
template := spec["template"].(map[string]interface{})
specCopy["template"] = template
template["$patch"] = "replace"
objCopy["spec"] = specCopy
patch, err := json.Marshal(objCopy)
return patch, err
}
// findHistory returns a controllerrevision of a specific revision from the given controllerrevisions.
// It returns nil if no such controllerrevision exists.
// If toRevision is 0, the last previously used history is returned.
func findHistory(toRevision int64, allHistory []*appsv1.ControllerRevision) *appsv1.ControllerRevision {
if toRevision == 0 && len(allHistory) <= 1 {
return nil
}
// Find the history to rollback to
var toHistory *appsv1.ControllerRevision
if toRevision == 0 {
// If toRevision == 0, find the latest revision (2nd max)
sort.Sort(historiesByRevision(allHistory))
toHistory = allHistory[len(allHistory)-2]
} else {
for _, h := range allHistory {
if h.Revision == toRevision {
// If toRevision != 0, find the history with matching revision
return h
}
}
}
return toHistory
}
// printPodTemplate converts a given pod template into a human-readable string.
func printPodTemplate(specTemplate *corev1.PodTemplateSpec) (string, error) {
podSpec, err := printTemplate(specTemplate)
if err != nil {
return "", err
}
return fmt.Sprintf("will roll back to %s", podSpec), nil
}
func revisionNotFoundErr(r int64) error {
return fmt.Errorf("unable to find specified revision %v in history", r)
}
// TODO: copied from daemon controller, should extract to a library
type historiesByRevision []*appsv1.ControllerRevision
func (h historiesByRevision) Len() int { return len(h) }
func (h historiesByRevision) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
func (h historiesByRevision) Less(i, j int) bool {
return h[i].Revision < h[j].Revision
}

View File

@ -0,0 +1,37 @@
/*
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 polymorphichelpers
import (
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/client-go/kubernetes"
)
// Returns a Rollbacker for changing the rollback version of the specified RESTMapping type or an error
func rollbacker(restClientGetter genericclioptions.RESTClientGetter, mapping *meta.RESTMapping) (Rollbacker, error) {
clientConfig, err := restClientGetter.ToRESTConfig()
if err != nil {
return nil, err
}
external, err := kubernetes.NewForConfig(clientConfig)
if err != nil {
return nil, err
}
return RollbackerFor(mapping.GroupVersionKind.GroupKind(), external)
}

View File

@ -0,0 +1,152 @@
/*
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 polymorphichelpers
import (
"fmt"
appsv1 "k8s.io/api/apps/v1"
extensionsv1beta1 "k8s.io/api/extensions/v1beta1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
deploymentutil "k8s.io/kubectl/pkg/util/deployment"
)
// StatusViewer provides an interface for resources that have rollout status.
type StatusViewer interface {
Status(obj runtime.Unstructured, revision int64) (string, bool, error)
}
// StatusViewerFor returns a StatusViewer for the resource specified by kind.
func StatusViewerFor(kind schema.GroupKind) (StatusViewer, error) {
switch kind {
case extensionsv1beta1.SchemeGroupVersion.WithKind("Deployment").GroupKind(),
appsv1.SchemeGroupVersion.WithKind("Deployment").GroupKind():
return &DeploymentStatusViewer{}, nil
case extensionsv1beta1.SchemeGroupVersion.WithKind("DaemonSet").GroupKind(),
appsv1.SchemeGroupVersion.WithKind("DaemonSet").GroupKind():
return &DaemonSetStatusViewer{}, nil
case appsv1.SchemeGroupVersion.WithKind("StatefulSet").GroupKind():
return &StatefulSetStatusViewer{}, nil
}
return nil, fmt.Errorf("no status viewer has been implemented for %v", kind)
}
// DeploymentStatusViewer implements the StatusViewer interface.
type DeploymentStatusViewer struct{}
// DaemonSetStatusViewer implements the StatusViewer interface.
type DaemonSetStatusViewer struct{}
// StatefulSetStatusViewer implements the StatusViewer interface.
type StatefulSetStatusViewer struct{}
// Status returns a message describing deployment status, and a bool value indicating if the status is considered done.
func (s *DeploymentStatusViewer) Status(obj runtime.Unstructured, revision int64) (string, bool, error) {
deployment := &appsv1.Deployment{}
err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.UnstructuredContent(), deployment)
if err != nil {
return "", false, fmt.Errorf("failed to convert %T to %T: %v", obj, deployment, err)
}
if revision > 0 {
deploymentRev, err := deploymentutil.Revision(deployment)
if err != nil {
return "", false, fmt.Errorf("cannot get the revision of deployment %q: %v", deployment.Name, err)
}
if revision != deploymentRev {
return "", false, fmt.Errorf("desired revision (%d) is different from the running revision (%d)", revision, deploymentRev)
}
}
if deployment.Generation <= deployment.Status.ObservedGeneration {
cond := deploymentutil.GetDeploymentCondition(deployment.Status, appsv1.DeploymentProgressing)
if cond != nil && cond.Reason == deploymentutil.TimedOutReason {
return "", false, fmt.Errorf("deployment %q exceeded its progress deadline", deployment.Name)
}
if deployment.Spec.Replicas != nil && deployment.Status.UpdatedReplicas < *deployment.Spec.Replicas {
return fmt.Sprintf("Waiting for deployment %q rollout to finish: %d out of %d new replicas have been updated...\n", deployment.Name, deployment.Status.UpdatedReplicas, *deployment.Spec.Replicas), false, nil
}
if deployment.Status.Replicas > deployment.Status.UpdatedReplicas {
return fmt.Sprintf("Waiting for deployment %q rollout to finish: %d old replicas are pending termination...\n", deployment.Name, deployment.Status.Replicas-deployment.Status.UpdatedReplicas), false, nil
}
if deployment.Status.AvailableReplicas < deployment.Status.UpdatedReplicas {
return fmt.Sprintf("Waiting for deployment %q rollout to finish: %d of %d updated replicas are available...\n", deployment.Name, deployment.Status.AvailableReplicas, deployment.Status.UpdatedReplicas), false, nil
}
return fmt.Sprintf("deployment %q successfully rolled out\n", deployment.Name), true, nil
}
return fmt.Sprintf("Waiting for deployment spec update to be observed...\n"), false, nil
}
// Status returns a message describing daemon set status, and a bool value indicating if the status is considered done.
func (s *DaemonSetStatusViewer) Status(obj runtime.Unstructured, revision int64) (string, bool, error) {
//ignoring revision as DaemonSets does not have history yet
daemon := &appsv1.DaemonSet{}
err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.UnstructuredContent(), daemon)
if err != nil {
return "", false, fmt.Errorf("failed to convert %T to %T: %v", obj, daemon, err)
}
if daemon.Spec.UpdateStrategy.Type != appsv1.RollingUpdateDaemonSetStrategyType {
return "", true, fmt.Errorf("rollout status is only available for %s strategy type", appsv1.RollingUpdateStatefulSetStrategyType)
}
if daemon.Generation <= daemon.Status.ObservedGeneration {
if daemon.Status.UpdatedNumberScheduled < daemon.Status.DesiredNumberScheduled {
return fmt.Sprintf("Waiting for daemon set %q rollout to finish: %d out of %d new pods have been updated...\n", daemon.Name, daemon.Status.UpdatedNumberScheduled, daemon.Status.DesiredNumberScheduled), false, nil
}
if daemon.Status.NumberAvailable < daemon.Status.DesiredNumberScheduled {
return fmt.Sprintf("Waiting for daemon set %q rollout to finish: %d of %d updated pods are available...\n", daemon.Name, daemon.Status.NumberAvailable, daemon.Status.DesiredNumberScheduled), false, nil
}
return fmt.Sprintf("daemon set %q successfully rolled out\n", daemon.Name), true, nil
}
return fmt.Sprintf("Waiting for daemon set spec update to be observed...\n"), false, nil
}
// Status returns a message describing statefulset status, and a bool value indicating if the status is considered done.
func (s *StatefulSetStatusViewer) Status(obj runtime.Unstructured, revision int64) (string, bool, error) {
sts := &appsv1.StatefulSet{}
err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.UnstructuredContent(), sts)
if err != nil {
return "", false, fmt.Errorf("failed to convert %T to %T: %v", obj, sts, err)
}
if sts.Spec.UpdateStrategy.Type != appsv1.RollingUpdateStatefulSetStrategyType {
return "", true, fmt.Errorf("rollout status is only available for %s strategy type", appsv1.RollingUpdateStatefulSetStrategyType)
}
if sts.Status.ObservedGeneration == 0 || sts.Generation > sts.Status.ObservedGeneration {
return "Waiting for statefulset spec update to be observed...\n", false, nil
}
if sts.Spec.Replicas != nil && sts.Status.ReadyReplicas < *sts.Spec.Replicas {
return fmt.Sprintf("Waiting for %d pods to be ready...\n", *sts.Spec.Replicas-sts.Status.ReadyReplicas), false, nil
}
if sts.Spec.UpdateStrategy.Type == appsv1.RollingUpdateStatefulSetStrategyType && sts.Spec.UpdateStrategy.RollingUpdate != nil {
if sts.Spec.Replicas != nil && sts.Spec.UpdateStrategy.RollingUpdate.Partition != nil {
if sts.Status.UpdatedReplicas < (*sts.Spec.Replicas - *sts.Spec.UpdateStrategy.RollingUpdate.Partition) {
return fmt.Sprintf("Waiting for partitioned roll out to finish: %d out of %d new pods have been updated...\n",
sts.Status.UpdatedReplicas, *sts.Spec.Replicas-*sts.Spec.UpdateStrategy.RollingUpdate.Partition), false, nil
}
}
return fmt.Sprintf("partitioned roll out complete: %d new pods have been updated...\n",
sts.Status.UpdatedReplicas), true, nil
}
if sts.Status.UpdateRevision != sts.Status.CurrentRevision {
return fmt.Sprintf("waiting for statefulset rolling update to complete %d pods at revision %s...\n",
sts.Status.UpdatedReplicas, sts.Status.UpdateRevision), false, nil
}
return fmt.Sprintf("statefulset rolling update complete %d pods at revision %s...\n", sts.Status.CurrentReplicas, sts.Status.CurrentRevision), true, nil
}

View File

@ -0,0 +1,26 @@
/*
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 polymorphichelpers
import (
"k8s.io/apimachinery/pkg/api/meta"
)
// statusViewer returns a StatusViewer for printing rollout status.
func statusViewer(mapping *meta.RESTMapping) (StatusViewer, error) {
return StatusViewerFor(mapping.GroupVersionKind.GroupKind())
}

View File

@ -0,0 +1,90 @@
/*
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 polymorphichelpers
import (
"fmt"
appsv1 "k8s.io/api/apps/v1"
appsv1beta1 "k8s.io/api/apps/v1beta1"
appsv1beta2 "k8s.io/api/apps/v1beta2"
batchv1 "k8s.io/api/batch/v1"
batchv1beta1 "k8s.io/api/batch/v1beta1"
"k8s.io/api/core/v1"
extensionsv1beta1 "k8s.io/api/extensions/v1beta1"
"k8s.io/apimachinery/pkg/runtime"
)
func updatePodSpecForObject(obj runtime.Object, fn func(*v1.PodSpec) error) (bool, error) {
switch t := obj.(type) {
case *v1.Pod:
return true, fn(&t.Spec)
// ReplicationController
case *v1.ReplicationController:
if t.Spec.Template == nil {
t.Spec.Template = &v1.PodTemplateSpec{}
}
return true, fn(&t.Spec.Template.Spec)
// Deployment
case *extensionsv1beta1.Deployment:
return true, fn(&t.Spec.Template.Spec)
case *appsv1beta1.Deployment:
return true, fn(&t.Spec.Template.Spec)
case *appsv1beta2.Deployment:
return true, fn(&t.Spec.Template.Spec)
case *appsv1.Deployment:
return true, fn(&t.Spec.Template.Spec)
// DaemonSet
case *extensionsv1beta1.DaemonSet:
return true, fn(&t.Spec.Template.Spec)
case *appsv1beta2.DaemonSet:
return true, fn(&t.Spec.Template.Spec)
case *appsv1.DaemonSet:
return true, fn(&t.Spec.Template.Spec)
// ReplicaSet
case *extensionsv1beta1.ReplicaSet:
return true, fn(&t.Spec.Template.Spec)
case *appsv1beta2.ReplicaSet:
return true, fn(&t.Spec.Template.Spec)
case *appsv1.ReplicaSet:
return true, fn(&t.Spec.Template.Spec)
// StatefulSet
case *appsv1beta1.StatefulSet:
return true, fn(&t.Spec.Template.Spec)
case *appsv1beta2.StatefulSet:
return true, fn(&t.Spec.Template.Spec)
case *appsv1.StatefulSet:
return true, fn(&t.Spec.Template.Spec)
// Job
case *batchv1.Job:
return true, fn(&t.Spec.Template.Spec)
// CronJob
case *batchv1beta1.CronJob:
return true, fn(&t.Spec.JobTemplate.Spec.Template.Spec)
case *batchv1.CronJob:
return true, fn(&t.Spec.JobTemplate.Spec.Template.Spec)
default:
return false, fmt.Errorf("the object is not a pod or does not have a pod template: %T", t)
}
}

View File

@ -0,0 +1,38 @@
/*
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 certificate
import (
"crypto/x509"
"encoding/pem"
"errors"
)
// TODO(yue9944882): Remove this helper package once it's copied to k/api
// ParseCSR extracts the CSR from the API object and decodes it.
func ParseCSR(pemBytes []byte) (*x509.CertificateRequest, error) {
block, _ := pem.Decode(pemBytes)
if block == nil || block.Type != "CERTIFICATE REQUEST" {
return nil, errors.New("PEM block type must be CERTIFICATE REQUEST")
}
csr, err := x509.ParseCertificateRequest(block.Bytes)
if err != nil {
return nil, err
}
return csr, nil
}

257
vendor/k8s.io/kubectl/pkg/util/deployment/deployment.go generated vendored Normal file
View File

@ -0,0 +1,257 @@
/*
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 deployment
import (
"context"
"sort"
"strconv"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
apiequality "k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
intstrutil "k8s.io/apimachinery/pkg/util/intstr"
runtimeresource "k8s.io/cli-runtime/pkg/resource"
appsclient "k8s.io/client-go/kubernetes/typed/apps/v1"
)
const (
// RevisionAnnotation is the revision annotation of a deployment's replica sets which records its rollout sequence
RevisionAnnotation = "deployment.kubernetes.io/revision"
// RevisionHistoryAnnotation maintains the history of all old revisions that a replica set has served for a deployment.
RevisionHistoryAnnotation = "deployment.kubernetes.io/revision-history"
// DesiredReplicasAnnotation is the desired replicas for a deployment recorded as an annotation
// in its replica sets. Helps in separating scaling events from the rollout process and for
// determining if the new replica set for a deployment is really saturated.
DesiredReplicasAnnotation = "deployment.kubernetes.io/desired-replicas"
// MaxReplicasAnnotation is the maximum replicas a deployment can have at a given point, which
// is deployment.spec.replicas + maxSurge. Used by the underlying replica sets to estimate their
// proportions in case the deployment has surge replicas.
MaxReplicasAnnotation = "deployment.kubernetes.io/max-replicas"
// RollbackRevisionNotFound is not found rollback event reason
RollbackRevisionNotFound = "DeploymentRollbackRevisionNotFound"
// RollbackTemplateUnchanged is the template unchanged rollback event reason
RollbackTemplateUnchanged = "DeploymentRollbackTemplateUnchanged"
// RollbackDone is the done rollback event reason
RollbackDone = "DeploymentRollback"
// TimedOutReason is added in a deployment when its newest replica set fails to show any progress
// within the given deadline (progressDeadlineSeconds).
TimedOutReason = "ProgressDeadlineExceeded"
)
// GetDeploymentCondition returns the condition with the provided type.
func GetDeploymentCondition(status appsv1.DeploymentStatus, condType appsv1.DeploymentConditionType) *appsv1.DeploymentCondition {
for i := range status.Conditions {
c := status.Conditions[i]
if c.Type == condType {
return &c
}
}
return nil
}
// Revision returns the revision number of the input object.
func Revision(obj runtime.Object) (int64, error) {
acc, err := meta.Accessor(obj)
if err != nil {
return 0, err
}
v, ok := acc.GetAnnotations()[RevisionAnnotation]
if !ok {
return 0, nil
}
return strconv.ParseInt(v, 10, 64)
}
// GetAllReplicaSets returns the old and new replica sets targeted by the given Deployment. It gets PodList and
// ReplicaSetList from client interface. Note that the first set of old replica sets doesn't include the ones
// with no pods, and the second set of old replica sets include all old replica sets. The third returned value
// is the new replica set, and it may be nil if it doesn't exist yet.
func GetAllReplicaSets(deployment *appsv1.Deployment, c appsclient.AppsV1Interface) ([]*appsv1.ReplicaSet, []*appsv1.ReplicaSet, *appsv1.ReplicaSet, error) {
rsList, err := listReplicaSets(deployment, rsListFromClient(c), nil)
if err != nil {
return nil, nil, nil, err
}
newRS := findNewReplicaSet(deployment, rsList)
oldRSes, allOldRSes := findOldReplicaSets(deployment, rsList, newRS)
return oldRSes, allOldRSes, newRS, nil
}
// GetAllReplicaSetsInChunks is the same as GetAllReplicaSets, but accepts a chunk size argument.
// It returns the old and new replica sets targeted by the given Deployment. It gets PodList and
// ReplicaSetList from client interface. Note that the first set of old replica sets doesn't include the ones
// with no pods, and the second set of old replica sets include all old replica sets. The third returned value
// is the new replica set, and it may be nil if it doesn't exist yet.
func GetAllReplicaSetsInChunks(deployment *appsv1.Deployment, c appsclient.AppsV1Interface, chunkSize int64) ([]*appsv1.ReplicaSet, []*appsv1.ReplicaSet, *appsv1.ReplicaSet, error) {
rsList, err := listReplicaSets(deployment, rsListFromClient(c), &chunkSize)
if err != nil {
return nil, nil, nil, err
}
newRS := findNewReplicaSet(deployment, rsList)
oldRSes, allOldRSes := findOldReplicaSets(deployment, rsList, newRS)
return oldRSes, allOldRSes, newRS, nil
}
// RsListFromClient returns an rsListFunc that wraps the given client.
func rsListFromClient(c appsclient.AppsV1Interface) rsListFunc {
return func(namespace string, initialOpts metav1.ListOptions) ([]*appsv1.ReplicaSet, error) {
rsList := &appsv1.ReplicaSetList{}
err := runtimeresource.FollowContinue(&initialOpts,
func(opts metav1.ListOptions) (runtime.Object, error) {
newRs, err := c.ReplicaSets(namespace).List(context.TODO(), opts)
if err != nil {
return nil, runtimeresource.EnhanceListError(err, opts, "replicasets")
}
rsList.Items = append(rsList.Items, newRs.Items...)
return newRs, nil
})
if err != nil {
return nil, err
}
var ret []*appsv1.ReplicaSet
for i := range rsList.Items {
ret = append(ret, &rsList.Items[i])
}
return ret, err
}
}
// TODO: switch this to full namespacers
type rsListFunc func(string, metav1.ListOptions) ([]*appsv1.ReplicaSet, error)
// listReplicaSets returns a slice of RSes the given deployment targets.
// Note that this does NOT attempt to reconcile ControllerRef (adopt/orphan),
// because only the controller itself should do that.
// However, it does filter out anything whose ControllerRef doesn't match.
func listReplicaSets(deployment *appsv1.Deployment, getRSList rsListFunc, chunkSize *int64) ([]*appsv1.ReplicaSet, error) {
// TODO: Right now we list replica sets by their labels. We should list them by selector, i.e. the replica set's selector
// should be a superset of the deployment's selector, see https://github.com/kubernetes/kubernetes/issues/19830.
namespace := deployment.Namespace
selector, err := metav1.LabelSelectorAsSelector(deployment.Spec.Selector)
if err != nil {
return nil, err
}
options := metav1.ListOptions{LabelSelector: selector.String()}
if chunkSize != nil {
options.Limit = *chunkSize
}
all, err := getRSList(namespace, options)
if err != nil {
return nil, err
}
// Only include those whose ControllerRef matches the Deployment.
owned := make([]*appsv1.ReplicaSet, 0, len(all))
for _, rs := range all {
if metav1.IsControlledBy(rs, deployment) {
owned = append(owned, rs)
}
}
return owned, nil
}
// EqualIgnoreHash returns true if two given podTemplateSpec are equal, ignoring the diff in value of Labels[pod-template-hash]
// We ignore pod-template-hash because:
// 1. The hash result would be different upon podTemplateSpec API changes
// (e.g. the addition of a new field will cause the hash code to change)
// 2. The deployment template won't have hash labels
func equalIgnoreHash(template1, template2 *corev1.PodTemplateSpec) bool {
t1Copy := template1.DeepCopy()
t2Copy := template2.DeepCopy()
// Remove hash labels from template.Labels before comparing
delete(t1Copy.Labels, appsv1.DefaultDeploymentUniqueLabelKey)
delete(t2Copy.Labels, appsv1.DefaultDeploymentUniqueLabelKey)
return apiequality.Semantic.DeepEqual(t1Copy, t2Copy)
}
// FindNewReplicaSet returns the new RS this given deployment targets (the one with the same pod template).
func findNewReplicaSet(deployment *appsv1.Deployment, rsList []*appsv1.ReplicaSet) *appsv1.ReplicaSet {
sort.Sort(replicaSetsByCreationTimestamp(rsList))
for i := range rsList {
if equalIgnoreHash(&rsList[i].Spec.Template, &deployment.Spec.Template) {
// In rare cases, such as after cluster upgrades, Deployment may end up with
// having more than one new ReplicaSets that have the same template as its template,
// see https://github.com/kubernetes/kubernetes/issues/40415
// We deterministically choose the oldest new ReplicaSet.
return rsList[i]
}
}
// new ReplicaSet does not exist.
return nil
}
// replicaSetsByCreationTimestamp sorts a list of ReplicaSet by creation timestamp, using their names as a tie breaker.
type replicaSetsByCreationTimestamp []*appsv1.ReplicaSet
func (o replicaSetsByCreationTimestamp) Len() int { return len(o) }
func (o replicaSetsByCreationTimestamp) Swap(i, j int) { o[i], o[j] = o[j], o[i] }
func (o replicaSetsByCreationTimestamp) Less(i, j int) bool {
if o[i].CreationTimestamp.Equal(&o[j].CreationTimestamp) {
return o[i].Name < o[j].Name
}
return o[i].CreationTimestamp.Before(&o[j].CreationTimestamp)
}
// // FindOldReplicaSets returns the old replica sets targeted by the given Deployment, with the given slice of RSes.
// // Note that the first set of old replica sets doesn't include the ones with no pods, and the second set of old replica sets include all old replica sets.
func findOldReplicaSets(deployment *appsv1.Deployment, rsList []*appsv1.ReplicaSet, newRS *appsv1.ReplicaSet) ([]*appsv1.ReplicaSet, []*appsv1.ReplicaSet) {
var requiredRSs []*appsv1.ReplicaSet
var allRSs []*appsv1.ReplicaSet
for _, rs := range rsList {
// Filter out new replica set
if newRS != nil && rs.UID == newRS.UID {
continue
}
allRSs = append(allRSs, rs)
if *(rs.Spec.Replicas) != 0 {
requiredRSs = append(requiredRSs, rs)
}
}
return requiredRSs, allRSs
}
// ResolveFenceposts resolves both maxSurge and maxUnavailable. This needs to happen in one
// step. For example:
//
// 2 desired, max unavailable 1%, surge 0% - should scale old(-1), then new(+1), then old(-1), then new(+1)
// 1 desired, max unavailable 1%, surge 0% - should scale old(-1), then new(+1)
// 2 desired, max unavailable 25%, surge 1% - should scale new(+1), then old(-1), then new(+1), then old(-1)
// 1 desired, max unavailable 25%, surge 1% - should scale new(+1), then old(-1)
// 2 desired, max unavailable 0%, surge 1% - should scale new(+1), then old(-1), then new(+1), then old(-1)
// 1 desired, max unavailable 0%, surge 1% - should scale new(+1), then old(-1)
func ResolveFenceposts(maxSurge, maxUnavailable *intstrutil.IntOrString, desired int32) (int32, int32, error) {
surge, err := intstrutil.GetScaledValueFromIntOrPercent(intstrutil.ValueOrDefault(maxSurge, intstrutil.FromInt(0)), int(desired), true)
if err != nil {
return 0, 0, err
}
unavailable, err := intstrutil.GetScaledValueFromIntOrPercent(intstrutil.ValueOrDefault(maxUnavailable, intstrutil.FromInt(0)), int(desired), false)
if err != nil {
return 0, 0, err
}
if surge == 0 && unavailable == 0 {
// Validation should never allow the user to explicitly use zero values for both maxSurge
// maxUnavailable. Due to rounding down maxUnavailable though, it may resolve to zero.
// If both fenceposts resolve to zero, then we should set maxUnavailable to 1 on the
// theory that surge might not work due to quota.
unavailable = 1
}
return int32(surge), int32(unavailable), nil
}

View File

@ -0,0 +1,36 @@
/*
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 event
import (
corev1 "k8s.io/api/core/v1"
)
// SortableEvents implements sort.Interface for []api.Event based on the Timestamp field
type SortableEvents []corev1.Event
func (list SortableEvents) Len() int {
return len(list)
}
func (list SortableEvents) Swap(i, j int) {
list[i], list[j] = list[j], list[i]
}
func (list SortableEvents) Less(i, j int) bool {
return list[i].LastTimestamp.Time.Before(list[j].LastTimestamp.Time)
}

111
vendor/k8s.io/kubectl/pkg/util/fieldpath/fieldpath.go generated vendored Normal file
View File

@ -0,0 +1,111 @@
/*
Copyright 2015 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 fieldpath
import (
"fmt"
"strings"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/validation"
)
// TODO(yue9944882): Remove this helper package once it's copied to k/apimachinery
// FormatMap formats map[string]string to a string.
func FormatMap(m map[string]string) (fmtStr string) {
// output with keys in sorted order to provide stable output
keys := sets.NewString()
for key := range m {
keys.Insert(key)
}
for _, key := range keys.List() {
fmtStr += fmt.Sprintf("%v=%q\n", key, m[key])
}
fmtStr = strings.TrimSuffix(fmtStr, "\n")
return
}
// ExtractFieldPathAsString extracts the field from the given object
// and returns it as a string. The object must be a pointer to an
// API type.
func ExtractFieldPathAsString(obj interface{}, fieldPath string) (string, error) {
accessor, err := meta.Accessor(obj)
if err != nil {
return "", nil
}
if path, subscript, ok := SplitMaybeSubscriptedPath(fieldPath); ok {
switch path {
case "metadata.annotations":
if errs := validation.IsQualifiedName(strings.ToLower(subscript)); len(errs) != 0 {
return "", fmt.Errorf("invalid key subscript in %s: %s", fieldPath, strings.Join(errs, ";"))
}
return accessor.GetAnnotations()[subscript], nil
case "metadata.labels":
if errs := validation.IsQualifiedName(subscript); len(errs) != 0 {
return "", fmt.Errorf("invalid key subscript in %s: %s", fieldPath, strings.Join(errs, ";"))
}
return accessor.GetLabels()[subscript], nil
default:
return "", fmt.Errorf("fieldPath %q does not support subscript", fieldPath)
}
}
switch fieldPath {
case "metadata.annotations":
return FormatMap(accessor.GetAnnotations()), nil
case "metadata.labels":
return FormatMap(accessor.GetLabels()), nil
case "metadata.name":
return accessor.GetName(), nil
case "metadata.namespace":
return accessor.GetNamespace(), nil
case "metadata.uid":
return string(accessor.GetUID()), nil
}
return "", fmt.Errorf("unsupported fieldPath: %v", fieldPath)
}
// SplitMaybeSubscriptedPath checks whether the specified fieldPath is
// subscripted, and
// - if yes, this function splits the fieldPath into path and subscript, and
// returns (path, subscript, true).
// - if no, this function returns (fieldPath, "", false).
//
// Example inputs and outputs:
// - "metadata.annotations['myKey']" --> ("metadata.annotations", "myKey", true)
// - "metadata.annotations['a[b]c']" --> ("metadata.annotations", "a[b]c", true)
// - "metadata.labels['']" --> ("metadata.labels", "", true)
// - "metadata.labels" --> ("metadata.labels", "", false)
func SplitMaybeSubscriptedPath(fieldPath string) (string, string, bool) {
if !strings.HasSuffix(fieldPath, "']") {
return fieldPath, "", false
}
s := strings.TrimSuffix(fieldPath, "']")
parts := strings.SplitN(s, "['", 2)
if len(parts) < 2 {
return fieldPath, "", false
}
if len(parts[0]) == 0 {
return fieldPath, "", false
}
return parts[0], parts[1], true
}

188
vendor/k8s.io/kubectl/pkg/util/podutils/podutils.go generated vendored Normal file
View File

@ -0,0 +1,188 @@
/*
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 podutils
import (
"time"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/utils/integer"
)
// IsPodAvailable returns true if a pod is available; false otherwise.
// Precondition for an available pod is that it must be ready. On top
// of that, there are two cases when a pod can be considered available:
// 1. minReadySeconds == 0, or
// 2. LastTransitionTime (is set) + minReadySeconds < current time
func IsPodAvailable(pod *corev1.Pod, minReadySeconds int32, now metav1.Time) bool {
if !IsPodReady(pod) {
return false
}
c := getPodReadyCondition(pod.Status)
minReadySecondsDuration := time.Duration(minReadySeconds) * time.Second
if minReadySeconds == 0 || !c.LastTransitionTime.IsZero() && c.LastTransitionTime.Add(minReadySecondsDuration).Before(now.Time) {
return true
}
return false
}
// IsPodReady returns true if a pod is ready; false otherwise.
func IsPodReady(pod *corev1.Pod) bool {
return isPodReadyConditionTrue(pod.Status)
}
// IsPodReadyConditionTrue returns true if a pod is ready; false otherwise.
func isPodReadyConditionTrue(status corev1.PodStatus) bool {
condition := getPodReadyCondition(status)
return condition != nil && condition.Status == corev1.ConditionTrue
}
// GetPodReadyCondition extracts the pod ready condition from the given status and returns that.
// Returns nil if the condition is not present.
func getPodReadyCondition(status corev1.PodStatus) *corev1.PodCondition {
_, condition := getPodCondition(&status, corev1.PodReady)
return condition
}
// GetPodCondition extracts the provided condition from the given status and returns that.
// Returns nil and -1 if the condition is not present, and the index of the located condition.
func getPodCondition(status *corev1.PodStatus, conditionType corev1.PodConditionType) (int, *corev1.PodCondition) {
if status == nil {
return -1, nil
}
return getPodConditionFromList(status.Conditions, conditionType)
}
// GetPodConditionFromList extracts the provided condition from the given list of condition and
// returns the index of the condition and the condition. Returns -1 and nil if the condition is not present.
func getPodConditionFromList(conditions []corev1.PodCondition, conditionType corev1.PodConditionType) (int, *corev1.PodCondition) {
if conditions == nil {
return -1, nil
}
for i := range conditions {
if conditions[i].Type == conditionType {
return i, &conditions[i]
}
}
return -1, nil
}
// ByLogging allows custom sorting of pods so the best one can be picked for getting its logs.
type ByLogging []*corev1.Pod
func (s ByLogging) Len() int { return len(s) }
func (s ByLogging) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
func (s ByLogging) Less(i, j int) bool {
// 1. assigned < unassigned
if s[i].Spec.NodeName != s[j].Spec.NodeName && (len(s[i].Spec.NodeName) == 0 || len(s[j].Spec.NodeName) == 0) {
return len(s[i].Spec.NodeName) > 0
}
// 2. PodRunning < PodUnknown < PodPending
m := map[corev1.PodPhase]int{corev1.PodRunning: 0, corev1.PodUnknown: 1, corev1.PodPending: 2}
if m[s[i].Status.Phase] != m[s[j].Status.Phase] {
return m[s[i].Status.Phase] < m[s[j].Status.Phase]
}
// 3. ready < not ready
if IsPodReady(s[i]) != IsPodReady(s[j]) {
return IsPodReady(s[i])
}
// TODO: take availability into account when we push minReadySeconds information from deployment into pods,
// see https://github.com/kubernetes/kubernetes/issues/22065
// 4. Been ready for more time < less time < empty time
if IsPodReady(s[i]) && IsPodReady(s[j]) && !podReadyTime(s[i]).Equal(podReadyTime(s[j])) {
return afterOrZero(podReadyTime(s[j]), podReadyTime(s[i]))
}
// 5. Pods with containers with higher restart counts < lower restart counts
if maxContainerRestarts(s[i]) != maxContainerRestarts(s[j]) {
return maxContainerRestarts(s[i]) > maxContainerRestarts(s[j])
}
// 6. older pods < newer pods < empty timestamp pods
if !s[i].CreationTimestamp.Equal(&s[j].CreationTimestamp) {
return afterOrZero(&s[j].CreationTimestamp, &s[i].CreationTimestamp)
}
return false
}
// ActivePods type allows custom sorting of pods so a controller can pick the best ones to delete.
type ActivePods []*corev1.Pod
func (s ActivePods) Len() int { return len(s) }
func (s ActivePods) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
func (s ActivePods) Less(i, j int) bool {
// 1. Unassigned < assigned
// If only one of the pods is unassigned, the unassigned one is smaller
if s[i].Spec.NodeName != s[j].Spec.NodeName && (len(s[i].Spec.NodeName) == 0 || len(s[j].Spec.NodeName) == 0) {
return len(s[i].Spec.NodeName) == 0
}
// 2. PodPending < PodUnknown < PodRunning
m := map[corev1.PodPhase]int{corev1.PodPending: 0, corev1.PodUnknown: 1, corev1.PodRunning: 2}
if m[s[i].Status.Phase] != m[s[j].Status.Phase] {
return m[s[i].Status.Phase] < m[s[j].Status.Phase]
}
// 3. Not ready < ready
// If only one of the pods is not ready, the not ready one is smaller
if IsPodReady(s[i]) != IsPodReady(s[j]) {
return !IsPodReady(s[i])
}
// TODO: take availability into account when we push minReadySeconds information from deployment into pods,
// see https://github.com/kubernetes/kubernetes/issues/22065
// 4. Been ready for empty time < less time < more time
// If both pods are ready, the latest ready one is smaller
if IsPodReady(s[i]) && IsPodReady(s[j]) && !podReadyTime(s[i]).Equal(podReadyTime(s[j])) {
return afterOrZero(podReadyTime(s[i]), podReadyTime(s[j]))
}
// 5. Pods with containers with higher restart counts < lower restart counts
if maxContainerRestarts(s[i]) != maxContainerRestarts(s[j]) {
return maxContainerRestarts(s[i]) > maxContainerRestarts(s[j])
}
// 6. Empty creation time pods < newer pods < older pods
if !s[i].CreationTimestamp.Equal(&s[j].CreationTimestamp) {
return afterOrZero(&s[i].CreationTimestamp, &s[j].CreationTimestamp)
}
return false
}
// afterOrZero checks if time t1 is after time t2; if one of them
// is zero, the zero time is seen as after non-zero time.
func afterOrZero(t1, t2 *metav1.Time) bool {
if t1.Time.IsZero() || t2.Time.IsZero() {
return t1.Time.IsZero()
}
return t1.After(t2.Time)
}
func podReadyTime(pod *corev1.Pod) *metav1.Time {
for _, c := range pod.Status.Conditions {
// we only care about pod ready conditions
if c.Type == corev1.PodReady && c.Status == corev1.ConditionTrue {
return &c.LastTransitionTime
}
}
return &metav1.Time{}
}
func maxContainerRestarts(pod *corev1.Pod) int {
maxRestarts := 0
for _, c := range pod.Status.ContainerStatuses {
maxRestarts = integer.IntMax(maxRestarts, int(c.RestartCount))
}
return maxRestarts
}

98
vendor/k8s.io/kubectl/pkg/util/qos/qos.go generated vendored Normal file
View File

@ -0,0 +1,98 @@
/*
Copyright 2015 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 qos
import (
core "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
"k8s.io/apimachinery/pkg/util/sets"
)
var supportedQoSComputeResources = sets.NewString(string(core.ResourceCPU), string(core.ResourceMemory))
func isSupportedQoSComputeResource(name core.ResourceName) bool {
return supportedQoSComputeResources.Has(string(name))
}
// GetPodQOS returns the QoS class of a pod.
// A pod is besteffort if none of its containers have specified any requests or limits.
// A pod is guaranteed only when requests and limits are specified for all the containers and they are equal.
// A pod is burstable if limits and requests do not match across all containers.
func GetPodQOS(pod *core.Pod) core.PodQOSClass {
requests := core.ResourceList{}
limits := core.ResourceList{}
zeroQuantity := resource.MustParse("0")
isGuaranteed := true
allContainers := []core.Container{}
allContainers = append(allContainers, pod.Spec.Containers...)
allContainers = append(allContainers, pod.Spec.InitContainers...)
for _, container := range allContainers {
// process requests
for name, quantity := range container.Resources.Requests {
if !isSupportedQoSComputeResource(name) {
continue
}
if quantity.Cmp(zeroQuantity) == 1 {
delta := quantity.DeepCopy()
if _, exists := requests[name]; !exists {
requests[name] = delta
} else {
delta.Add(requests[name])
requests[name] = delta
}
}
}
// process limits
qosLimitsFound := sets.NewString()
for name, quantity := range container.Resources.Limits {
if !isSupportedQoSComputeResource(name) {
continue
}
if quantity.Cmp(zeroQuantity) == 1 {
qosLimitsFound.Insert(string(name))
delta := quantity.DeepCopy()
if _, exists := limits[name]; !exists {
limits[name] = delta
} else {
delta.Add(limits[name])
limits[name] = delta
}
}
}
if !qosLimitsFound.HasAll(string(core.ResourceMemory), string(core.ResourceCPU)) {
isGuaranteed = false
}
}
if len(requests) == 0 && len(limits) == 0 {
return core.PodQOSBestEffort
}
// Check is requests match limits for all resources.
if isGuaranteed {
for name, req := range requests {
if lim, exists := limits[name]; !exists || lim.Cmp(req) != 0 {
isGuaranteed = false
break
}
}
}
if isGuaranteed &&
len(requests) == len(limits) {
return core.PodQOSGuaranteed
}
return core.PodQOSBurstable
}

135
vendor/k8s.io/kubectl/pkg/util/rbac/rbac.go generated vendored Normal file
View File

@ -0,0 +1,135 @@
/*
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 rbac
import (
rbacv1 "k8s.io/api/rbac/v1"
"k8s.io/apimachinery/pkg/util/sets"
"reflect"
"strings"
)
type simpleResource struct {
Group string
Resource string
ResourceNameExist bool
ResourceName string
}
// CompactRules combines rules that contain a single APIGroup/Resource, differ only by verb, and contain no other attributes.
// this is a fast check, and works well with the decomposed "missing rules" list from a Covers check.
func CompactRules(rules []rbacv1.PolicyRule) ([]rbacv1.PolicyRule, error) {
compacted := make([]rbacv1.PolicyRule, 0, len(rules))
simpleRules := map[simpleResource]*rbacv1.PolicyRule{}
for _, rule := range rules {
if resource, isSimple := isSimpleResourceRule(&rule); isSimple {
if existingRule, ok := simpleRules[resource]; ok {
// Add the new verbs to the existing simple resource rule
if existingRule.Verbs == nil {
existingRule.Verbs = []string{}
}
existingVerbs := sets.NewString(existingRule.Verbs...)
for _, verb := range rule.Verbs {
if !existingVerbs.Has(verb) {
existingRule.Verbs = append(existingRule.Verbs, verb)
}
}
} else {
// Copy the rule to accumulate matching simple resource rules into
simpleRules[resource] = rule.DeepCopy()
}
} else {
compacted = append(compacted, rule)
}
}
// Once we've consolidated the simple resource rules, add them to the compacted list
for _, simpleRule := range simpleRules {
compacted = append(compacted, *simpleRule)
}
return compacted, nil
}
// isSimpleResourceRule returns true if the given rule contains verbs, a single resource, a single API group, at most one Resource Name, and no other values
func isSimpleResourceRule(rule *rbacv1.PolicyRule) (simpleResource, bool) {
resource := simpleResource{}
// If we have "complex" rule attributes, return early without allocations or expensive comparisons
if len(rule.ResourceNames) > 1 || len(rule.NonResourceURLs) > 0 {
return resource, false
}
// If we have multiple api groups or resources, return early
if len(rule.APIGroups) != 1 || len(rule.Resources) != 1 {
return resource, false
}
// Test if this rule only contains APIGroups/Resources/Verbs/ResourceNames
simpleRule := &rbacv1.PolicyRule{APIGroups: rule.APIGroups, Resources: rule.Resources, Verbs: rule.Verbs, ResourceNames: rule.ResourceNames}
if !reflect.DeepEqual(simpleRule, rule) {
return resource, false
}
if len(rule.ResourceNames) == 0 {
resource = simpleResource{Group: rule.APIGroups[0], Resource: rule.Resources[0], ResourceNameExist: false}
} else {
resource = simpleResource{Group: rule.APIGroups[0], Resource: rule.Resources[0], ResourceNameExist: true, ResourceName: rule.ResourceNames[0]}
}
return resource, true
}
// BreakdownRule takes a rule and builds an equivalent list of rules that each have at most one verb, one
// resource, and one resource name
func BreakdownRule(rule rbacv1.PolicyRule) []rbacv1.PolicyRule {
subrules := []rbacv1.PolicyRule{}
for _, group := range rule.APIGroups {
for _, resource := range rule.Resources {
for _, verb := range rule.Verbs {
if len(rule.ResourceNames) > 0 {
for _, resourceName := range rule.ResourceNames {
subrules = append(subrules, rbacv1.PolicyRule{APIGroups: []string{group}, Resources: []string{resource}, Verbs: []string{verb}, ResourceNames: []string{resourceName}})
}
} else {
subrules = append(subrules, rbacv1.PolicyRule{APIGroups: []string{group}, Resources: []string{resource}, Verbs: []string{verb}})
}
}
}
}
// Non-resource URLs are unique because they only combine with verbs.
for _, nonResourceURL := range rule.NonResourceURLs {
for _, verb := range rule.Verbs {
subrules = append(subrules, rbacv1.PolicyRule{NonResourceURLs: []string{nonResourceURL}, Verbs: []string{verb}})
}
}
return subrules
}
// SortableRuleSlice is used to sort rule slice
type SortableRuleSlice []rbacv1.PolicyRule
func (s SortableRuleSlice) Len() int { return len(s) }
func (s SortableRuleSlice) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
func (s SortableRuleSlice) Less(i, j int) bool {
return strings.Compare(s[i].String(), s[j].String()) < 0
}

172
vendor/k8s.io/kubectl/pkg/util/resource/resource.go generated vendored Normal file
View File

@ -0,0 +1,172 @@
/*
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 resource
import (
"fmt"
"math"
"strconv"
"strings"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
"k8s.io/apimachinery/pkg/util/sets"
)
// PodRequestsAndLimits returns a dictionary of all defined resources summed up for all
// containers of the pod. If pod overhead is non-nil, the pod overhead is added to the
// total container resource requests and to the total container limits which have a
// non-zero quantity.
func PodRequestsAndLimits(pod *corev1.Pod) (reqs, limits corev1.ResourceList) {
reqs, limits = corev1.ResourceList{}, corev1.ResourceList{}
for _, container := range pod.Spec.Containers {
addResourceList(reqs, container.Resources.Requests)
addResourceList(limits, container.Resources.Limits)
}
// init containers define the minimum of any resource
for _, container := range pod.Spec.InitContainers {
maxResourceList(reqs, container.Resources.Requests)
maxResourceList(limits, container.Resources.Limits)
}
// Add overhead for running a pod to the sum of requests and to non-zero limits:
if pod.Spec.Overhead != nil {
addResourceList(reqs, pod.Spec.Overhead)
for name, quantity := range pod.Spec.Overhead {
if value, ok := limits[name]; ok && !value.IsZero() {
value.Add(quantity)
limits[name] = value
}
}
}
return
}
// addResourceList adds the resources in newList to list
func addResourceList(list, new corev1.ResourceList) {
for name, quantity := range new {
if value, ok := list[name]; !ok {
list[name] = quantity.DeepCopy()
} else {
value.Add(quantity)
list[name] = value
}
}
}
// maxResourceList sets list to the greater of list/newList for every resource
// either list
func maxResourceList(list, new corev1.ResourceList) {
for name, quantity := range new {
if value, ok := list[name]; !ok {
list[name] = quantity.DeepCopy()
continue
} else {
if quantity.Cmp(value) > 0 {
list[name] = quantity.DeepCopy()
}
}
}
}
// ExtractContainerResourceValue extracts the value of a resource
// in an already known container
func ExtractContainerResourceValue(fs *corev1.ResourceFieldSelector, container *corev1.Container) (string, error) {
divisor := resource.Quantity{}
if divisor.Cmp(fs.Divisor) == 0 {
divisor = resource.MustParse("1")
} else {
divisor = fs.Divisor
}
switch fs.Resource {
case "limits.cpu":
return convertResourceCPUToString(container.Resources.Limits.Cpu(), divisor)
case "limits.memory":
return convertResourceMemoryToString(container.Resources.Limits.Memory(), divisor)
case "limits.ephemeral-storage":
return convertResourceEphemeralStorageToString(container.Resources.Limits.StorageEphemeral(), divisor)
case "requests.cpu":
return convertResourceCPUToString(container.Resources.Requests.Cpu(), divisor)
case "requests.memory":
return convertResourceMemoryToString(container.Resources.Requests.Memory(), divisor)
case "requests.ephemeral-storage":
return convertResourceEphemeralStorageToString(container.Resources.Requests.StorageEphemeral(), divisor)
}
// handle extended standard resources with dynamic names
// example: requests.hugepages-<pageSize> or limits.hugepages-<pageSize>
if strings.HasPrefix(fs.Resource, "requests.") {
resourceName := corev1.ResourceName(strings.TrimPrefix(fs.Resource, "requests."))
if IsHugePageResourceName(resourceName) {
return convertResourceHugePagesToString(container.Resources.Requests.Name(resourceName, resource.BinarySI), divisor)
}
}
if strings.HasPrefix(fs.Resource, "limits.") {
resourceName := corev1.ResourceName(strings.TrimPrefix(fs.Resource, "limits."))
if IsHugePageResourceName(resourceName) {
return convertResourceHugePagesToString(container.Resources.Limits.Name(resourceName, resource.BinarySI), divisor)
}
}
return "", fmt.Errorf("Unsupported container resource : %v", fs.Resource)
}
// convertResourceCPUToString converts cpu value to the format of divisor and returns
// ceiling of the value.
func convertResourceCPUToString(cpu *resource.Quantity, divisor resource.Quantity) (string, error) {
c := int64(math.Ceil(float64(cpu.MilliValue()) / float64(divisor.MilliValue())))
return strconv.FormatInt(c, 10), nil
}
// convertResourceMemoryToString converts memory value to the format of divisor and returns
// ceiling of the value.
func convertResourceMemoryToString(memory *resource.Quantity, divisor resource.Quantity) (string, error) {
m := int64(math.Ceil(float64(memory.Value()) / float64(divisor.Value())))
return strconv.FormatInt(m, 10), nil
}
// convertResourceHugePagesToString converts hugepages value to the format of divisor and returns
// ceiling of the value.
func convertResourceHugePagesToString(hugePages *resource.Quantity, divisor resource.Quantity) (string, error) {
m := int64(math.Ceil(float64(hugePages.Value()) / float64(divisor.Value())))
return strconv.FormatInt(m, 10), nil
}
// convertResourceEphemeralStorageToString converts ephemeral storage value to the format of divisor and returns
// ceiling of the value.
func convertResourceEphemeralStorageToString(ephemeralStorage *resource.Quantity, divisor resource.Quantity) (string, error) {
m := int64(math.Ceil(float64(ephemeralStorage.Value()) / float64(divisor.Value())))
return strconv.FormatInt(m, 10), nil
}
var standardContainerResources = sets.NewString(
string(corev1.ResourceCPU),
string(corev1.ResourceMemory),
string(corev1.ResourceEphemeralStorage),
)
// IsStandardContainerResourceName returns true if the container can make a resource request
// for the specified resource
func IsStandardContainerResourceName(str string) bool {
return standardContainerResources.Has(str) || IsHugePageResourceName(corev1.ResourceName(str))
}
// IsHugePageResourceName returns true if the resource name has the huge page
// resource prefix.
func IsHugePageResourceName(name corev1.ResourceName) bool {
return strings.HasPrefix(string(name), corev1.ResourceHugePagesPrefix)
}

38
vendor/k8s.io/kubectl/pkg/util/slice/slice.go generated vendored Normal file
View File

@ -0,0 +1,38 @@
/*
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 slice
import (
"sort"
)
// SortInts64 sorts []int64 in increasing order
func SortInts64(a []int64) { sort.Slice(a, func(i, j int) bool { return a[i] < a[j] }) }
// ContainsString checks if a given slice of strings contains the provided string.
// If a modifier func is provided, it is called with the slice item before the comparation.
func ContainsString(slice []string, s string, modifier func(s string) string) bool {
for _, item := range slice {
if item == s {
return true
}
if modifier != nil && modifier(item) == s {
return true
}
}
return false
}

110
vendor/k8s.io/kubectl/pkg/util/storage/storage.go generated vendored Normal file
View File

@ -0,0 +1,110 @@
/*
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 storage
import (
"k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"strings"
)
// TODO(yue9944882): Remove this helper package once it's copied to k/api
// IsDefaultStorageClassAnnotation represents a StorageClass annotation that
// marks a class as the default StorageClass
const IsDefaultStorageClassAnnotation = "storageclass.kubernetes.io/is-default-class"
// BetaIsDefaultStorageClassAnnotation is the beta version of IsDefaultStorageClassAnnotation.
const BetaIsDefaultStorageClassAnnotation = "storageclass.beta.kubernetes.io/is-default-class"
// IsDefaultAnnotationText returns a pretty Yes/No String if
// the annotation is set
func IsDefaultAnnotationText(obj metav1.ObjectMeta) string {
if obj.Annotations[IsDefaultStorageClassAnnotation] == "true" {
return "Yes"
}
if obj.Annotations[BetaIsDefaultStorageClassAnnotation] == "true" {
return "Yes"
}
return "No"
}
// GetAccessModesAsString returns a string representation of an array of access modes.
// modes, when present, are always in the same order: RWO,ROX,RWX,RWOP.
func GetAccessModesAsString(modes []v1.PersistentVolumeAccessMode) string {
modes = removeDuplicateAccessModes(modes)
modesStr := []string{}
if ContainsAccessMode(modes, v1.ReadWriteOnce) {
modesStr = append(modesStr, "RWO")
}
if ContainsAccessMode(modes, v1.ReadOnlyMany) {
modesStr = append(modesStr, "ROX")
}
if ContainsAccessMode(modes, v1.ReadWriteMany) {
modesStr = append(modesStr, "RWX")
}
if ContainsAccessMode(modes, v1.ReadWriteOncePod) {
modesStr = append(modesStr, "RWOP")
}
return strings.Join(modesStr, ",")
}
// removeDuplicateAccessModes returns an array of access modes without any duplicates
func removeDuplicateAccessModes(modes []v1.PersistentVolumeAccessMode) []v1.PersistentVolumeAccessMode {
accessModes := []v1.PersistentVolumeAccessMode{}
for _, m := range modes {
if !ContainsAccessMode(accessModes, m) {
accessModes = append(accessModes, m)
}
}
return accessModes
}
func ContainsAccessMode(modes []v1.PersistentVolumeAccessMode, mode v1.PersistentVolumeAccessMode) bool {
for _, m := range modes {
if m == mode {
return true
}
}
return false
}
// GetPersistentVolumeClass returns StorageClassName.
func GetPersistentVolumeClass(volume *v1.PersistentVolume) string {
// Use beta annotation first
if class, found := volume.Annotations[v1.BetaStorageClassAnnotation]; found {
return class
}
return volume.Spec.StorageClassName
}
// GetPersistentVolumeClaimClass returns StorageClassName. If no storage class was
// requested, it returns "".
func GetPersistentVolumeClaimClass(claim *v1.PersistentVolumeClaim) string {
// Use beta annotation first
if class, found := claim.Annotations[v1.BetaStorageClassAnnotation]; found {
return class
}
if claim.Spec.StorageClassName != nil {
return *claim.Spec.StorageClassName
}
return ""
}

18
vendor/modules.txt vendored
View File

@ -67,6 +67,9 @@ github.com/evanphx/json-patch/v5
# github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d
## explicit
github.com/exponent-io/jsonpath
# github.com/fatih/camelcase v1.0.0
## explicit
github.com/fatih/camelcase
# github.com/felixge/httpsnoop v1.0.1
## explicit; go 1.13
github.com/felixge/httpsnoop
@ -1259,6 +1262,7 @@ k8s.io/client-go/tools/watch
k8s.io/client-go/transport
k8s.io/client-go/transport/spdy
k8s.io/client-go/util/cert
k8s.io/client-go/util/certificate/csr
k8s.io/client-go/util/connrotation
k8s.io/client-go/util/exec
k8s.io/client-go/util/flowcontrol
@ -1373,15 +1377,29 @@ k8s.io/kube-openapi/pkg/util/sets
k8s.io/kube-openapi/pkg/validation/spec
# k8s.io/kubectl v0.23.4
## explicit; go 1.16
k8s.io/kubectl/pkg/apps
k8s.io/kubectl/pkg/cmd/apiresources
k8s.io/kubectl/pkg/cmd/get
k8s.io/kubectl/pkg/cmd/util
k8s.io/kubectl/pkg/cmd/util/podcmd
k8s.io/kubectl/pkg/describe
k8s.io/kubectl/pkg/polymorphichelpers
k8s.io/kubectl/pkg/rawhttp
k8s.io/kubectl/pkg/scheme
k8s.io/kubectl/pkg/util/certificate
k8s.io/kubectl/pkg/util/deployment
k8s.io/kubectl/pkg/util/event
k8s.io/kubectl/pkg/util/fieldpath
k8s.io/kubectl/pkg/util/i18n
k8s.io/kubectl/pkg/util/interrupt
k8s.io/kubectl/pkg/util/openapi
k8s.io/kubectl/pkg/util/openapi/validation
k8s.io/kubectl/pkg/util/podutils
k8s.io/kubectl/pkg/util/qos
k8s.io/kubectl/pkg/util/rbac
k8s.io/kubectl/pkg/util/resource
k8s.io/kubectl/pkg/util/slice
k8s.io/kubectl/pkg/util/storage
k8s.io/kubectl/pkg/util/templates
k8s.io/kubectl/pkg/util/term
k8s.io/kubectl/pkg/validation