Add logs function to karmadactl
Signed-off-by: lonelyCZ <531187475@qq.com>
This commit is contained in:
parent
a3c334c3b1
commit
a52bfca0b8
1
go.mod
1
go.mod
|
@ -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
1
go.sum
|
@ -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=
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
language: go
|
||||
go: 1.x
|
||||
|
|
@ -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.
|
|
@ -0,0 +1,58 @@
|
|||
# CamelCase [](http://godoc.org/github.com/fatih/camelcase) [](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"]
|
||||
```
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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, ", ")
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -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)
|
||||
}
|
54
vendor/k8s.io/kubectl/pkg/polymorphichelpers/attachablepodforobject.go
generated
vendored
Normal file
54
vendor/k8s.io/kubectl/pkg/polymorphichelpers/attachablepodforobject.go
generated
vendored
Normal 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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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]
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
|
@ -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
|
||||
}
|
160
vendor/k8s.io/kubectl/pkg/polymorphichelpers/mapbasedselectorforobject.go
generated
vendored
Normal file
160
vendor/k8s.io/kubectl/pkg/polymorphichelpers/mapbasedselectorforobject.go
generated
vendored
Normal 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, ",")
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
89
vendor/k8s.io/kubectl/pkg/polymorphichelpers/protocolsforobject.go
generated
vendored
Normal file
89
vendor/k8s.io/kubectl/pkg/polymorphichelpers/protocolsforobject.go
generated
vendored
Normal 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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
|
||||
}
|
|
@ -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())
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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 ""
|
||||
}
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue