Feature/rollout cloneset (#1)
* add rollout for cloneset Signed-off-by: hantmac <hantmac@outlook.com> * add Kruise Copyright
This commit is contained in:
parent
8638a9cde9
commit
f9288f1a8f
|
|
@ -0,0 +1,18 @@
|
||||||
|
.PHONY: build plugin check
|
||||||
|
|
||||||
|
LDFLAGS = $(shell ./version.sh)
|
||||||
|
GOENV := GO15VENDOREXPERIMENT="1" GO111MODULE=on CGO_ENABLED=0 GOOS=${GOOS} GOARCH=amd64
|
||||||
|
GO := $(GOENV) go
|
||||||
|
|
||||||
|
default: build
|
||||||
|
|
||||||
|
build: plugin
|
||||||
|
|
||||||
|
plugin:
|
||||||
|
GO111MODULE=on CGO_ENABLED=0 GOOS=${GOOS} go build -ldflags '$(LDFLAGS)' -o kubectl-kruise cmd/plugin/main.go
|
||||||
|
|
||||||
|
|
||||||
|
check:
|
||||||
|
find . -iname '*.go' -type f | grep -v /vendor/ | xargs gofmt -l
|
||||||
|
GO111MODULE=on go test -v -race ./...
|
||||||
|
$(GO) vet ./...
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 The Kruise 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 main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/openkruise/kruise-tools/pkg/cmd"
|
||||||
|
"github.com/spf13/pflag"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
flags := pflag.NewFlagSet("kubectl-kruise", pflag.ExitOnError)
|
||||||
|
pflag.CommandLine = flags
|
||||||
|
|
||||||
|
root := cmd.NewDefaultKubectlCommand()
|
||||||
|
if err := root.Execute(); err != nil {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
43
go.mod
43
go.mod
|
|
@ -1,14 +1,39 @@
|
||||||
module github.com/openkruise/kruise-tools
|
module github.com/openkruise/kruise-tools
|
||||||
|
|
||||||
go 1.13
|
go 1.16
|
||||||
|
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/openkruise/kruise-api v0.5.0
|
github.com/coreos/bbolt v1.3.3 // indirect
|
||||||
github.com/spf13/cobra v0.0.5
|
github.com/coreos/etcd v3.3.17+incompatible // indirect
|
||||||
k8s.io/api v0.15.8
|
github.com/golangplus/bytes v0.0.0-20160111154220-45c989fe5450 // indirect
|
||||||
k8s.io/apimachinery v0.15.8
|
github.com/golangplus/fmt v0.0.0-20150411045040-2a5d6d7d2995 // indirect
|
||||||
k8s.io/cli-runtime v0.15.8
|
github.com/lithammer/dedent v1.1.0
|
||||||
k8s.io/client-go v0.15.8
|
github.com/openkruise/kruise-api v0.8.0
|
||||||
k8s.io/klog v1.0.0
|
github.com/pkg/errors v0.9.1
|
||||||
sigs.k8s.io/controller-runtime v0.3.0
|
github.com/spf13/cobra v1.1.3
|
||||||
|
github.com/spf13/pflag v1.0.5
|
||||||
|
github.com/xlab/handysort v0.0.0-20150421192137-fb3537ed64a1 // indirect
|
||||||
|
k8s.io/api v0.21.0
|
||||||
|
k8s.io/apimachinery v0.21.0
|
||||||
|
k8s.io/cli-runtime v0.21.0
|
||||||
|
k8s.io/client-go v0.21.0
|
||||||
|
k8s.io/component-base v0.21.0
|
||||||
|
k8s.io/klog v1.0.0 // indirect
|
||||||
|
k8s.io/kubectl v0.21.0
|
||||||
|
sigs.k8s.io/controller-runtime v0.6.3
|
||||||
|
sigs.k8s.io/structured-merge-diff v1.0.1 // indirect
|
||||||
|
sigs.k8s.io/structured-merge-diff/v2 v2.0.1 // indirect
|
||||||
|
vbom.ml/util v0.0.0-20160121211510-db5cfe13f5cc // indirect
|
||||||
|
)
|
||||||
|
|
||||||
|
// Replace to match K8s 1.18.6
|
||||||
|
replace (
|
||||||
|
k8s.io/api => k8s.io/api v0.18.6
|
||||||
|
k8s.io/apimachinery => k8s.io/apimachinery v0.18.6
|
||||||
|
k8s.io/cli-runtime => k8s.io/cli-runtime v0.18.6
|
||||||
|
k8s.io/client-go => k8s.io/client-go v0.18.6
|
||||||
|
k8s.io/component-base => k8s.io/component-base v0.18.6
|
||||||
|
k8s.io/kubectl v0.21.0 => k8s.io/kubectl v0.18.6
|
||||||
|
k8s.io/metrics => k8s.io/metrics v0.18.6
|
||||||
)
|
)
|
||||||
|
|
|
||||||
73
main.go
73
main.go
|
|
@ -1,73 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2020 The Kruise 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 main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"math/rand"
|
|
||||||
"os"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/openkruise/kruise-tools/cmd/migrate"
|
|
||||||
cmdutil "github.com/openkruise/kruise-tools/cmd/util"
|
|
||||||
"github.com/openkruise/kruise-tools/pkg/utils/logs"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
|
||||||
)
|
|
||||||
|
|
||||||
// noUsageError suppresses usage printing when it occurs
|
|
||||||
// (since cobra doesn't provide a good way to avoid printing
|
|
||||||
// out usage in only certain situations).
|
|
||||||
type noUsageError struct{ error }
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
rand.Seed(time.Now().UnixNano())
|
|
||||||
|
|
||||||
command := newCommand()
|
|
||||||
|
|
||||||
logs.InitLogs()
|
|
||||||
defer logs.FlushLogs()
|
|
||||||
|
|
||||||
if err := command.Execute(); err != nil {
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func newCommand() *cobra.Command {
|
|
||||||
ioStream := cmdutil.IOStreams{In: os.Stdin, Out: os.Stdout, ErrOut: os.Stderr}
|
|
||||||
|
|
||||||
cmds := &cobra.Command{
|
|
||||||
Use: "kruise",
|
|
||||||
Short: "kruise is a command-line tool to use Kruise.",
|
|
||||||
Long: "kruise is a command-line tool to use Kruise.",
|
|
||||||
Run: runHelp,
|
|
||||||
}
|
|
||||||
|
|
||||||
flags := cmds.PersistentFlags()
|
|
||||||
kubeConfigFlags := genericclioptions.NewConfigFlags(true).WithDeprecatedPasswordFlag()
|
|
||||||
kubeConfigFlags.AddFlags(flags)
|
|
||||||
f := cmdutil.NewFactory(kubeConfigFlags)
|
|
||||||
|
|
||||||
cmds.AddCommand(
|
|
||||||
migrate.NewCmdMigrate(f, ioStream),
|
|
||||||
)
|
|
||||||
|
|
||||||
return cmds
|
|
||||||
}
|
|
||||||
|
|
||||||
func runHelp(cmd *cobra.Command, args []string) {
|
|
||||||
cmd.Help()
|
|
||||||
}
|
|
||||||
|
|
@ -17,28 +17,49 @@ limitations under the License.
|
||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
appsv1alpha1 "github.com/openkruise/kruise-api/apps/v1alpha1"
|
"sync"
|
||||||
|
|
||||||
|
kruiseappsv1alpha1 "github.com/openkruise/kruise-api/apps/v1alpha1"
|
||||||
|
"github.com/pkg/errors"
|
||||||
apps "k8s.io/api/apps/v1"
|
apps "k8s.io/api/apps/v1"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
"k8s.io/apimachinery/pkg/types"
|
"k8s.io/apimachinery/pkg/types"
|
||||||
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
|
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
|
||||||
|
"k8s.io/kubectl/pkg/scheme"
|
||||||
|
ctrl "sigs.k8s.io/controller-runtime"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
scheme = runtime.NewScheme()
|
|
||||||
|
|
||||||
DeploymentKind = apps.SchemeGroupVersion.WithKind("Deployment")
|
DeploymentKind = apps.SchemeGroupVersion.WithKind("Deployment")
|
||||||
CloneSetKind = appsv1alpha1.SchemeGroupVersion.WithKind("CloneSet")
|
CloneSetKind = kruiseappsv1alpha1.SchemeGroupVersion.WithKind("CloneSet")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var managerOnce sync.Once
|
||||||
|
var mgr ctrl.Manager
|
||||||
|
var Scheme = scheme.Scheme
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
_ = clientgoscheme.AddToScheme(scheme)
|
_ = clientgoscheme.AddToScheme(Scheme)
|
||||||
_ = appsv1alpha1.AddToScheme(scheme)
|
_ = kruiseappsv1alpha1.AddToScheme(Scheme)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewManager() ctrl.Manager {
|
||||||
|
managerOnce.Do(func() {
|
||||||
|
var err error
|
||||||
|
mgr, err = ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
|
||||||
|
Scheme: Scheme,
|
||||||
|
MetricsBindAddress: "0",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
panic(errors.Wrap(err, "unable to start manager"))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return mgr
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetScheme() *runtime.Scheme {
|
func GetScheme() *runtime.Scheme {
|
||||||
return scheme
|
return Scheme
|
||||||
}
|
}
|
||||||
|
|
||||||
type ResourceRef struct {
|
type ResourceRef struct {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 The Kruise Authors.
|
||||||
|
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 cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||||
|
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||||
|
"k8s.io/kubectl/pkg/util/i18n"
|
||||||
|
"k8s.io/kubectl/pkg/util/templates"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewCmdAlpha creates a command that acts as an alternate root command for features in alpha
|
||||||
|
func NewCmdAlpha(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "alpha",
|
||||||
|
Short: i18n.T("Commands for features in alpha"),
|
||||||
|
Long: templates.LongDesc(i18n.T("These commands correspond to alpha features that are not enabled in Kubernetes clusters by default.")),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alpha commands should be added here. As features graduate from alpha they should move
|
||||||
|
// from here to the CommandGroups defined by NewKubeletCommand() in cmd.go.
|
||||||
|
|
||||||
|
// NewKubeletCommand() will hide the alpha command if it has no subcommands. Overriding
|
||||||
|
// the help function ensures a reasonable message if someone types the hidden command anyway.
|
||||||
|
if !cmd.HasAvailableSubCommands() {
|
||||||
|
cmd.SetHelpFunc(func(*cobra.Command, []string) {
|
||||||
|
cmd.Println(i18n.T("No alpha commands are available in this version of kubectl"))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,470 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 The Kruise 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 cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"github.com/openkruise/kruise-tools/pkg/cmd/migrate"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
krollout "github.com/openkruise/kruise-tools/pkg/cmd/rollout"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||||
|
"k8s.io/client-go/tools/clientcmd"
|
||||||
|
cliflag "k8s.io/component-base/cli/flag"
|
||||||
|
"k8s.io/kubectl/pkg/cmd/apiresources"
|
||||||
|
"k8s.io/kubectl/pkg/cmd/apply"
|
||||||
|
"k8s.io/kubectl/pkg/cmd/attach"
|
||||||
|
"k8s.io/kubectl/pkg/cmd/autoscale"
|
||||||
|
"k8s.io/kubectl/pkg/cmd/certificates"
|
||||||
|
"k8s.io/kubectl/pkg/cmd/clusterinfo"
|
||||||
|
cmdconfig "k8s.io/kubectl/pkg/cmd/config"
|
||||||
|
"k8s.io/kubectl/pkg/cmd/debug"
|
||||||
|
"k8s.io/kubectl/pkg/cmd/describe"
|
||||||
|
"k8s.io/kubectl/pkg/cmd/diff"
|
||||||
|
"k8s.io/kubectl/pkg/cmd/drain"
|
||||||
|
cmdexec "k8s.io/kubectl/pkg/cmd/exec"
|
||||||
|
"k8s.io/kubectl/pkg/cmd/kustomize"
|
||||||
|
"k8s.io/kubectl/pkg/cmd/logs"
|
||||||
|
"k8s.io/kubectl/pkg/cmd/options"
|
||||||
|
"k8s.io/kubectl/pkg/cmd/patch"
|
||||||
|
"k8s.io/kubectl/pkg/cmd/plugin"
|
||||||
|
"k8s.io/kubectl/pkg/cmd/portforward"
|
||||||
|
"k8s.io/kubectl/pkg/cmd/replace"
|
||||||
|
"k8s.io/kubectl/pkg/cmd/scale"
|
||||||
|
"k8s.io/kubectl/pkg/cmd/taint"
|
||||||
|
"k8s.io/kubectl/pkg/cmd/top"
|
||||||
|
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||||
|
"k8s.io/kubectl/pkg/cmd/version"
|
||||||
|
"k8s.io/kubectl/pkg/cmd/wait"
|
||||||
|
"k8s.io/kubectl/pkg/util/i18n"
|
||||||
|
"k8s.io/kubectl/pkg/util/templates"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
bashCompletionFunc = `# call kubectl get $1,
|
||||||
|
__kubectl_debug_out()
|
||||||
|
{
|
||||||
|
local cmd="$1"
|
||||||
|
__kubectl_debug "${FUNCNAME[1]}: get completion by ${cmd}"
|
||||||
|
eval "${cmd} 2>/dev/null"
|
||||||
|
}
|
||||||
|
|
||||||
|
__kubectl_override_flag_list=(--kubeconfig --cluster --user --context --namespace --server -n -s)
|
||||||
|
__kubectl_override_flags()
|
||||||
|
{
|
||||||
|
local ${__kubectl_override_flag_list[*]##*-} two_word_of of var
|
||||||
|
for w in "${words[@]}"; do
|
||||||
|
if [ -n "${two_word_of}" ]; then
|
||||||
|
eval "${two_word_of##*-}=\"${two_word_of}=\${w}\""
|
||||||
|
two_word_of=
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
for of in "${__kubectl_override_flag_list[@]}"; do
|
||||||
|
case "${w}" in
|
||||||
|
${of}=*)
|
||||||
|
eval "${of##*-}=\"${w}\""
|
||||||
|
;;
|
||||||
|
${of})
|
||||||
|
two_word_of="${of}"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
done
|
||||||
|
for var in "${__kubectl_override_flag_list[@]##*-}"; do
|
||||||
|
if eval "test -n \"\$${var}\""; then
|
||||||
|
eval "echo -n \${${var}}' '"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
__kubectl_config_get_contexts()
|
||||||
|
{
|
||||||
|
__kubectl_parse_config "contexts"
|
||||||
|
}
|
||||||
|
|
||||||
|
__kubectl_config_get_clusters()
|
||||||
|
{
|
||||||
|
__kubectl_parse_config "clusters"
|
||||||
|
}
|
||||||
|
|
||||||
|
__kubectl_config_get_users()
|
||||||
|
{
|
||||||
|
__kubectl_parse_config "users"
|
||||||
|
}
|
||||||
|
|
||||||
|
# $1 has to be "contexts", "clusters" or "users"
|
||||||
|
__kubectl_parse_config()
|
||||||
|
{
|
||||||
|
local template kubectl_out
|
||||||
|
template="{{ range .$1 }}{{ .name }} {{ end }}"
|
||||||
|
if kubectl_out=$(__kubectl_debug_out "kubectl config $(__kubectl_override_flags) -o template --template=\"${template}\" view"); then
|
||||||
|
COMPREPLY=( $( compgen -W "${kubectl_out[*]}" -- "$cur" ) )
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# $1 is the name of resource (required)
|
||||||
|
# $2 is template string for kubectl get (optional)
|
||||||
|
__kubectl_parse_get()
|
||||||
|
{
|
||||||
|
local template
|
||||||
|
template="${2:-"{{ range .items }}{{ .metadata.name }} {{ end }}"}"
|
||||||
|
local kubectl_out
|
||||||
|
if kubectl_out=$(__kubectl_debug_out "kubectl get $(__kubectl_override_flags) -o template --template=\"${template}\" \"$1\""); then
|
||||||
|
COMPREPLY+=( $( compgen -W "${kubectl_out[*]}" -- "$cur" ) )
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Same as __kubectl_get_resources (with s) but allows completion for only one resource name.
|
||||||
|
__kubectl_get_resource()
|
||||||
|
{
|
||||||
|
if [[ ${#nouns[@]} -eq 0 ]]; then
|
||||||
|
__kubectl_get_resource_helper "" "$cur"
|
||||||
|
return # the return status is that of the last command executed in the function body
|
||||||
|
fi
|
||||||
|
__kubectl_parse_get "${nouns[${#nouns[@]} -1]}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Same as __kubectl_get_resource (without s) but allows completion for multiple, comma-separated resource names.
|
||||||
|
__kubectl_get_resources()
|
||||||
|
{
|
||||||
|
local SEPARATOR=','
|
||||||
|
if [[ ${#nouns[@]} -eq 0 ]]; then
|
||||||
|
local kubectl_out HEAD TAIL
|
||||||
|
HEAD=""
|
||||||
|
TAIL="$cur"
|
||||||
|
# if SEPARATOR is contained in $cur, e.g. "pod,sec"
|
||||||
|
if [[ "$cur" = *${SEPARATOR}* ]] ; then
|
||||||
|
# set HEAD to "pod,"
|
||||||
|
HEAD="${cur%${SEPARATOR}*}${SEPARATOR}"
|
||||||
|
# set TAIL to "sec"
|
||||||
|
TAIL="${cur##*${SEPARATOR}}"
|
||||||
|
fi
|
||||||
|
__kubectl_get_resource_helper "$HEAD" "$TAIL"
|
||||||
|
return # the return status is that of the last command executed in the function body
|
||||||
|
fi
|
||||||
|
__kubectl_parse_get "${nouns[${#nouns[@]} -1]}"
|
||||||
|
}
|
||||||
|
|
||||||
|
__kubectl_get_resource_helper()
|
||||||
|
{
|
||||||
|
local kubectl_out HEAD TAIL
|
||||||
|
HEAD="$1"
|
||||||
|
TAIL="$2"
|
||||||
|
if kubectl_out=$(__kubectl_debug_out "kubectl api-resources $(__kubectl_override_flags) -o name --cached --request-timeout=5s --verbs=get"); then
|
||||||
|
COMPREPLY=( $( compgen -P "$HEAD" -W "${kubectl_out[*]}" -- "$TAIL" ) )
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
__kubectl_get_resource_namespace()
|
||||||
|
{
|
||||||
|
__kubectl_parse_get "namespace"
|
||||||
|
}
|
||||||
|
|
||||||
|
__kubectl_get_resource_pod()
|
||||||
|
{
|
||||||
|
__kubectl_parse_get "pod"
|
||||||
|
}
|
||||||
|
|
||||||
|
__kubectl_get_resource_rc()
|
||||||
|
{
|
||||||
|
__kubectl_parse_get "rc"
|
||||||
|
}
|
||||||
|
|
||||||
|
__kubectl_get_resource_node()
|
||||||
|
{
|
||||||
|
__kubectl_parse_get "node"
|
||||||
|
}
|
||||||
|
|
||||||
|
__kubectl_get_resource_clusterrole()
|
||||||
|
{
|
||||||
|
__kubectl_parse_get "clusterrole"
|
||||||
|
}
|
||||||
|
|
||||||
|
# $1 is the name of the pod we want to get the list of containers inside
|
||||||
|
__kubectl_get_containers()
|
||||||
|
{
|
||||||
|
local template
|
||||||
|
template="{{ range .spec.initContainers }}{{ .name }} {{end}}{{ range .spec.containers }}{{ .name }} {{ end }}"
|
||||||
|
__kubectl_debug "${FUNCNAME} nouns are ${nouns[*]}"
|
||||||
|
|
||||||
|
local len="${#nouns[@]}"
|
||||||
|
if [[ ${len} -ne 1 ]]; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
local last=${nouns[${len} -1]}
|
||||||
|
local kubectl_out
|
||||||
|
if kubectl_out=$(__kubectl_debug_out "kubectl get $(__kubectl_override_flags) -o template --template=\"${template}\" pods \"${last}\""); then
|
||||||
|
COMPREPLY=( $( compgen -W "${kubectl_out[*]}" -- "$cur" ) )
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Require both a pod and a container to be specified
|
||||||
|
__kubectl_require_pod_and_container()
|
||||||
|
{
|
||||||
|
if [[ ${#nouns[@]} -eq 0 ]]; then
|
||||||
|
__kubectl_parse_get pods
|
||||||
|
return 0
|
||||||
|
fi;
|
||||||
|
__kubectl_get_containers
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
__kubectl_cp()
|
||||||
|
{
|
||||||
|
if [[ $(type -t compopt) = "builtin" ]]; then
|
||||||
|
compopt -o nospace
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$cur" in
|
||||||
|
/*|[.~]*) # looks like a path
|
||||||
|
return
|
||||||
|
;;
|
||||||
|
*:*) # TODO: complete remote files in the pod
|
||||||
|
return
|
||||||
|
;;
|
||||||
|
*/*) # complete <namespace>/<pod>
|
||||||
|
local template namespace kubectl_out
|
||||||
|
template="{{ range .items }}{{ .metadata.namespace }}/{{ .metadata.name }}: {{ end }}"
|
||||||
|
namespace="${cur%%/*}"
|
||||||
|
if kubectl_out=$(__kubectl_debug_out "kubectl get $(__kubectl_override_flags) --namespace \"${namespace}\" -o template --template=\"${template}\" pods"); then
|
||||||
|
COMPREPLY=( $(compgen -W "${kubectl_out[*]}" -- "${cur}") )
|
||||||
|
fi
|
||||||
|
return
|
||||||
|
;;
|
||||||
|
*) # complete namespaces, pods, and filedirs
|
||||||
|
__kubectl_parse_get "namespace" "{{ range .items }}{{ .metadata.name }}/ {{ end }}"
|
||||||
|
__kubectl_parse_get "pod" "{{ range .items }}{{ .metadata.name }}: {{ end }}"
|
||||||
|
_filedir
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
__kubectl_custom_func() {
|
||||||
|
case ${last_command} in
|
||||||
|
kubectl_get)
|
||||||
|
__kubectl_get_resources
|
||||||
|
return
|
||||||
|
;;
|
||||||
|
kubectl_describe | kubectl_delete | kubectl_label | kubectl_edit | kubectl_patch |\
|
||||||
|
kubectl_annotate | kubectl_expose | kubectl_scale | kubectl_autoscale | kubectl_taint | kubectl_rollout_* |\
|
||||||
|
kubectl_apply_edit-last-applied | kubectl_apply_view-last-applied)
|
||||||
|
__kubectl_get_resource
|
||||||
|
return
|
||||||
|
;;
|
||||||
|
kubectl_logs)
|
||||||
|
__kubectl_require_pod_and_container
|
||||||
|
return
|
||||||
|
;;
|
||||||
|
kubectl_exec | kubectl_port-forward | kubectl_top_pod | kubectl_attach)
|
||||||
|
__kubectl_get_resource_pod
|
||||||
|
return
|
||||||
|
;;
|
||||||
|
kubectl_cordon | kubectl_uncordon | kubectl_drain | kubectl_top_node)
|
||||||
|
__kubectl_get_resource_node
|
||||||
|
return
|
||||||
|
;;
|
||||||
|
kubectl_config_use-context | kubectl_config_rename-context | kubectl_config_delete-context)
|
||||||
|
__kubectl_config_get_contexts
|
||||||
|
return
|
||||||
|
;;
|
||||||
|
kubectl_config_delete-cluster)
|
||||||
|
__kubectl_config_get_clusters
|
||||||
|
return
|
||||||
|
;;
|
||||||
|
kubectl_cp)
|
||||||
|
__kubectl_cp
|
||||||
|
return
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
`
|
||||||
|
)
|
||||||
|
|
||||||
|
const kubectlCmdHeaders = "KUBECTL_COMMAND_HEADERS"
|
||||||
|
|
||||||
|
var (
|
||||||
|
bashCompletionFlags = map[string]string{
|
||||||
|
"namespace": "__kubectl_get_resource_namespace",
|
||||||
|
"context": "__kubectl_config_get_contexts",
|
||||||
|
"cluster": "__kubectl_config_get_clusters",
|
||||||
|
"user": "__kubectl_config_get_users",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewKubectlCommand creates the `kubectl-kruise` command and its nested children.
|
||||||
|
func NewKubectlCommand(in io.Reader, out, err io.Writer) *cobra.Command {
|
||||||
|
warningsAsErrors := false
|
||||||
|
// Parent command to which all subcommands are added.
|
||||||
|
cmds := &cobra.Command{
|
||||||
|
Use: "kubectl-kruise",
|
||||||
|
Short: i18n.T("kubectl-kruise controls the OpenKruise manager"),
|
||||||
|
Aliases: []string{"kk"},
|
||||||
|
Long: templates.LongDesc(`
|
||||||
|
kubectl-kruise controls the OpenKruise manager.
|
||||||
|
|
||||||
|
Find more information at:
|
||||||
|
https://openkruise.io/en-us/docs/what_is_openkruise.html`),
|
||||||
|
Run: runHelp,
|
||||||
|
// Hook before and after Run initialize and write profiles to disk,
|
||||||
|
// respectively.
|
||||||
|
PersistentPreRunE: func(*cobra.Command, []string) error {
|
||||||
|
//rest.SetDefaultWarningHandler(warningHandler)
|
||||||
|
return initProfiling()
|
||||||
|
},
|
||||||
|
PersistentPostRunE: func(*cobra.Command, []string) error {
|
||||||
|
if err := flushProfiling(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
BashCompletionFunction: bashCompletionFunc,
|
||||||
|
}
|
||||||
|
|
||||||
|
flags := cmds.PersistentFlags()
|
||||||
|
flags.SetNormalizeFunc(cliflag.WarnWordSepNormalizeFunc) // Warn for "_" flags
|
||||||
|
|
||||||
|
// Normalize all flags that are coming from other packages or pre-configurations
|
||||||
|
// a.k.a. change all "_" to "-". e.g. glog package
|
||||||
|
flags.SetNormalizeFunc(cliflag.WordSepNormalizeFunc)
|
||||||
|
|
||||||
|
addProfilingFlags(flags)
|
||||||
|
|
||||||
|
flags.BoolVar(&warningsAsErrors, "warnings-as-errors", warningsAsErrors, "Treat warnings received from the server as errors and exit with a non-zero exit code")
|
||||||
|
|
||||||
|
kubeConfigFlags := genericclioptions.NewConfigFlags(true).WithDeprecatedPasswordFlag()
|
||||||
|
kubeConfigFlags.AddFlags(flags)
|
||||||
|
matchVersionKubeConfigFlags := cmdutil.NewMatchVersionFlags(kubeConfigFlags)
|
||||||
|
matchVersionKubeConfigFlags.AddFlags(cmds.PersistentFlags())
|
||||||
|
|
||||||
|
cmds.PersistentFlags().AddGoFlagSet(flag.CommandLine)
|
||||||
|
|
||||||
|
f := cmdutil.NewFactory(matchVersionKubeConfigFlags)
|
||||||
|
|
||||||
|
// Sending in 'nil' for the getLanguageFn() results in using
|
||||||
|
// the LANG environment variable.
|
||||||
|
//
|
||||||
|
// TODO: Consider adding a flag or file preference for setting
|
||||||
|
// the language, instead of just loading from the LANG env. variable.
|
||||||
|
i18n.LoadTranslations("kubectl", nil)
|
||||||
|
|
||||||
|
// From this point and forward we get warnings on flags that contain "_" separators
|
||||||
|
cmds.SetGlobalNormalizationFunc(cliflag.WarnWordSepNormalizeFunc)
|
||||||
|
|
||||||
|
ioStreams := genericclioptions.IOStreams{In: in, Out: out, ErrOut: err}
|
||||||
|
|
||||||
|
groups := templates.CommandGroups{
|
||||||
|
{
|
||||||
|
Message: "CloneSet Commands:",
|
||||||
|
Commands: []*cobra.Command{
|
||||||
|
krollout.NewCmdRollout(f, ioStreams),
|
||||||
|
scale.NewCmdScale(f, ioStreams),
|
||||||
|
autoscale.NewCmdAutoscale(f, ioStreams),
|
||||||
|
migrate.NewCmdMigrate(f, ioStreams),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Message: "Cluster Management Commands:",
|
||||||
|
Commands: []*cobra.Command{
|
||||||
|
certificates.NewCmdCertificate(f, ioStreams),
|
||||||
|
clusterinfo.NewCmdClusterInfo(f, ioStreams),
|
||||||
|
top.NewCmdTop(f, ioStreams),
|
||||||
|
drain.NewCmdCordon(f, ioStreams),
|
||||||
|
drain.NewCmdUncordon(f, ioStreams),
|
||||||
|
drain.NewCmdDrain(f, ioStreams),
|
||||||
|
taint.NewCmdTaint(f, ioStreams),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Message: "Troubleshooting and Debugging Commands:",
|
||||||
|
Commands: []*cobra.Command{
|
||||||
|
describe.NewCmdDescribe("kubectl-kruise", f, ioStreams),
|
||||||
|
logs.NewCmdLogs(f, ioStreams),
|
||||||
|
attach.NewCmdAttach(f, ioStreams),
|
||||||
|
cmdexec.NewCmdExec(f, ioStreams),
|
||||||
|
portforward.NewCmdPortForward(f, ioStreams),
|
||||||
|
debug.NewCmdDebug(f, ioStreams),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Message: "Advanced Commands:",
|
||||||
|
Commands: []*cobra.Command{
|
||||||
|
diff.NewCmdDiff(f, ioStreams),
|
||||||
|
apply.NewCmdApply("kubectl-kruise", f, ioStreams),
|
||||||
|
patch.NewCmdPatch(f, ioStreams),
|
||||||
|
replace.NewCmdReplace(f, ioStreams),
|
||||||
|
wait.NewCmdWait(f, ioStreams),
|
||||||
|
kustomize.NewCmdKustomize(ioStreams),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
groups.Add(cmds)
|
||||||
|
|
||||||
|
filters := []string{"options"}
|
||||||
|
|
||||||
|
// Hide the "alpha" subcommand if there are no alpha commands in this build.
|
||||||
|
alpha := NewCmdAlpha(f, ioStreams)
|
||||||
|
if !alpha.HasSubCommands() {
|
||||||
|
filters = append(filters, alpha.Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
templates.ActsAsRootCommand(cmds, filters, groups...)
|
||||||
|
|
||||||
|
for name, completion := range bashCompletionFlags {
|
||||||
|
if cmds.Flag(name) != nil {
|
||||||
|
if cmds.Flag(name).Annotations == nil {
|
||||||
|
cmds.Flag(name).Annotations = map[string][]string{}
|
||||||
|
}
|
||||||
|
cmds.Flag(name).Annotations[cobra.BashCompCustom] = append(
|
||||||
|
cmds.Flag(name).Annotations[cobra.BashCompCustom],
|
||||||
|
completion,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cmds.AddCommand(alpha)
|
||||||
|
cmds.AddCommand(cmdconfig.NewCmdConfig(f, clientcmd.NewDefaultPathOptions(), ioStreams))
|
||||||
|
cmds.AddCommand(plugin.NewCmdPlugin(f, ioStreams))
|
||||||
|
cmds.AddCommand(version.NewCmdVersion(f, ioStreams))
|
||||||
|
cmds.AddCommand(apiresources.NewCmdAPIVersions(f, ioStreams))
|
||||||
|
cmds.AddCommand(apiresources.NewCmdAPIResources(f, ioStreams))
|
||||||
|
cmds.AddCommand(options.NewCmdOptions(ioStreams.Out))
|
||||||
|
|
||||||
|
return cmds
|
||||||
|
}
|
||||||
|
|
||||||
|
func runHelp(cmd *cobra.Command, args []string) {
|
||||||
|
cmd.Help()
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDefaultKubectlCommand creates the `kubectl` command with default arguments
|
||||||
|
func NewDefaultKubectlCommand() *cobra.Command {
|
||||||
|
return NewDefaultKubectlCommandWithArgs(os.Args, os.Stdin, os.Stdout, os.Stderr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDefaultKubectlCommandWithArgs creates the `kubectl` command with arguments
|
||||||
|
func NewDefaultKubectlCommandWithArgs(args []string, in io.Reader, out, errout io.Writer) *cobra.Command {
|
||||||
|
cmd := NewKubectlCommand(in, out, errout)
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
@ -19,9 +19,10 @@ package migrate
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
cmdutil "github.com/openkruise/kruise-tools/cmd/util"
|
|
||||||
"github.com/openkruise/kruise-tools/pkg/api"
|
"github.com/openkruise/kruise-tools/pkg/api"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||||
|
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
type migrateOptions struct {
|
type migrateOptions struct {
|
||||||
|
|
@ -40,14 +41,14 @@ type migrateOptions struct {
|
||||||
MaxSurge int32
|
MaxSurge int32
|
||||||
TimeoutSeconds int32
|
TimeoutSeconds int32
|
||||||
|
|
||||||
cmdutil.IOStreams
|
genericclioptions.IOStreams
|
||||||
}
|
}
|
||||||
|
|
||||||
func newMigrateOptions(ioStreams cmdutil.IOStreams) *migrateOptions {
|
func newMigrateOptions(ioStreams genericclioptions.IOStreams) *migrateOptions {
|
||||||
return &migrateOptions{IOStreams: ioStreams}
|
return &migrateOptions{IOStreams: ioStreams}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCmdMigrate(f cmdutil.Factory, ioStreams cmdutil.IOStreams) *cobra.Command {
|
func NewCmdMigrate(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command {
|
||||||
o := newMigrateOptions(ioStreams)
|
o := newMigrateOptions(ioStreams)
|
||||||
|
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
|
|
@ -20,12 +20,13 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
cmdutil "github.com/openkruise/kruise-tools/cmd/util"
|
internalcmdutil "github.com/openkruise/kruise-tools/pkg/cmd/util"
|
||||||
"github.com/openkruise/kruise-tools/pkg/creation"
|
"github.com/openkruise/kruise-tools/pkg/creation"
|
||||||
clonesetcreation "github.com/openkruise/kruise-tools/pkg/creation/cloneset"
|
clonesetcreation "github.com/openkruise/kruise-tools/pkg/creation/cloneset"
|
||||||
"github.com/openkruise/kruise-tools/pkg/migration"
|
"github.com/openkruise/kruise-tools/pkg/migration"
|
||||||
clonesetmigration "github.com/openkruise/kruise-tools/pkg/migration/cloneset"
|
clonesetmigration "github.com/openkruise/kruise-tools/pkg/migration/cloneset"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (o *migrateOptions) migrateCloneSet(f cmdutil.Factory, cmd *cobra.Command) error {
|
func (o *migrateOptions) migrateCloneSet(f cmdutil.Factory, cmd *cobra.Command) error {
|
||||||
|
|
@ -46,7 +47,7 @@ func (o *migrateOptions) migrateCloneSet(f cmdutil.Factory, cmd *cobra.Command)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
cmdutil.Print(fmt.Sprintf("Successfully created from %s/%s to %s/%s", o.From, o.SrcName, o.To, o.DstName))
|
internalcmdutil.Print(fmt.Sprintf("Successfully created from %s/%s to %s/%s", o.From, o.SrcName, o.To, o.DstName))
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
|
|
@ -80,13 +81,13 @@ func (o *migrateOptions) migrateCloneSet(f cmdutil.Factory, cmd *cobra.Command)
|
||||||
}
|
}
|
||||||
|
|
||||||
if newResult.SrcMigratedReplicas != oldResult.SrcMigratedReplicas || newResult.DstMigratedReplicas != oldResult.DstMigratedReplicas {
|
if newResult.SrcMigratedReplicas != oldResult.SrcMigratedReplicas || newResult.DstMigratedReplicas != oldResult.DstMigratedReplicas {
|
||||||
cmdutil.Print(fmt.Sprintf("Migration progress: %s/%s scale in %d, %s/%s scale out %d",
|
internalcmdutil.Print(fmt.Sprintf("Migration progress: %s/%s scale in %d, %s/%s scale out %d",
|
||||||
o.From, o.SrcName, newResult.SrcMigratedReplicas, o.To, o.DstName, newResult.DstMigratedReplicas))
|
o.From, o.SrcName, newResult.SrcMigratedReplicas, o.To, o.DstName, newResult.DstMigratedReplicas))
|
||||||
}
|
}
|
||||||
|
|
||||||
switch newResult.State {
|
switch newResult.State {
|
||||||
case migration.MigrateSucceeded:
|
case migration.MigrateSucceeded:
|
||||||
cmdutil.Print(fmt.Sprintf("Successfully migrated %v replicas from %s/%s to %s/%s",
|
internalcmdutil.Print(fmt.Sprintf("Successfully migrated %v replicas from %s/%s to %s/%s",
|
||||||
newResult.DstMigratedReplicas, o.From, o.SrcName, o.To, o.DstName))
|
newResult.DstMigratedReplicas, o.From, o.SrcName, o.To, o.DstName))
|
||||||
return nil
|
return nil
|
||||||
case migration.MigrateFailed:
|
case migration.MigrateFailed:
|
||||||
|
|
@ -0,0 +1,101 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 The Kruise Authors.
|
||||||
|
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 cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"runtime"
|
||||||
|
"runtime/pprof"
|
||||||
|
|
||||||
|
"github.com/spf13/pflag"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
profileName string
|
||||||
|
profileOutput string
|
||||||
|
)
|
||||||
|
|
||||||
|
func addProfilingFlags(flags *pflag.FlagSet) {
|
||||||
|
flags.StringVar(&profileName, "profile", "none", "Name of profile to capture. One of (none|cpu|heap|goroutine|threadcreate|block|mutex)")
|
||||||
|
flags.StringVar(&profileOutput, "profile-output", "profile.pprof", "Name of the file to write the profile to")
|
||||||
|
}
|
||||||
|
|
||||||
|
func initProfiling() error {
|
||||||
|
switch profileName {
|
||||||
|
case "none":
|
||||||
|
return nil
|
||||||
|
case "cpu":
|
||||||
|
f, err := os.Create(profileOutput)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = pprof.StartCPUProfile(f)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Block and mutex profiles need a call to Set{Block,Mutex}ProfileRate to
|
||||||
|
// output anything. We choose to sample all events.
|
||||||
|
case "block":
|
||||||
|
runtime.SetBlockProfileRate(1)
|
||||||
|
case "mutex":
|
||||||
|
runtime.SetMutexProfileFraction(1)
|
||||||
|
default:
|
||||||
|
// Check the profile name is valid.
|
||||||
|
if profile := pprof.Lookup(profileName); profile == nil {
|
||||||
|
return fmt.Errorf("unknown profile '%s'", profileName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the command is interrupted before the end (ctrl-c), flush the
|
||||||
|
// profiling files
|
||||||
|
c := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(c, os.Interrupt)
|
||||||
|
go func() {
|
||||||
|
<-c
|
||||||
|
flushProfiling()
|
||||||
|
os.Exit(0)
|
||||||
|
}()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func flushProfiling() error {
|
||||||
|
switch profileName {
|
||||||
|
case "none":
|
||||||
|
return nil
|
||||||
|
case "cpu":
|
||||||
|
pprof.StopCPUProfile()
|
||||||
|
case "heap":
|
||||||
|
runtime.GC()
|
||||||
|
fallthrough
|
||||||
|
default:
|
||||||
|
profile := pprof.Lookup(profileName)
|
||||||
|
if profile == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
f, err := os.Create(profileOutput)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
profile.WriteTo(f, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
package rollout
|
||||||
|
|
||||||
|
/*
|
||||||
|
Copyright 2021 The Kruise Authors.
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/lithammer/dedent"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||||
|
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||||
|
"k8s.io/kubectl/pkg/util/i18n"
|
||||||
|
"k8s.io/kubectl/pkg/util/templates"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
rolloutLong = templates.LongDesc(i18n.T(`
|
||||||
|
Manage the rollout of a resource.`) + rolloutValidResources)
|
||||||
|
|
||||||
|
rolloutExample = templates.Examples(`
|
||||||
|
# Rollback to the previous cloneset
|
||||||
|
kubectl-kruise rollout undo cloneset/abc
|
||||||
|
|
||||||
|
# Check the rollout status of a daemonset
|
||||||
|
kubectl-kruise rollout status daemonset/foo`)
|
||||||
|
|
||||||
|
rolloutValidResources = dedent.Dedent(`
|
||||||
|
|
||||||
|
Valid resource types include:
|
||||||
|
|
||||||
|
* deployments
|
||||||
|
* daemonsets
|
||||||
|
* statefulsets
|
||||||
|
* clonesets
|
||||||
|
`)
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewCmdRollout returns a Command instance for 'rollout' sub command
|
||||||
|
func NewCmdRollout(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "rollout SUBCOMMAND",
|
||||||
|
DisableFlagsInUseLine: true,
|
||||||
|
Short: i18n.T("Manage the rollout of a resource"),
|
||||||
|
Long: rolloutLong,
|
||||||
|
Example: rolloutExample,
|
||||||
|
Run: cmdutil.DefaultSubCommandRun(streams.Out),
|
||||||
|
}
|
||||||
|
// subcommands
|
||||||
|
cmd.AddCommand(NewCmdRolloutHistory(f, streams))
|
||||||
|
cmd.AddCommand(NewCmdRolloutPause(f, streams))
|
||||||
|
cmd.AddCommand(NewCmdRolloutResume(f, streams))
|
||||||
|
cmd.AddCommand(NewCmdRolloutUndo(f, streams))
|
||||||
|
cmd.AddCommand(NewCmdRolloutStatus(f, streams))
|
||||||
|
cmd.AddCommand(NewCmdRolloutRestart(f, streams))
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,181 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 The Kruise Authors.
|
||||||
|
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 rollout
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
internalapi "github.com/openkruise/kruise-tools/pkg/api"
|
||||||
|
|
||||||
|
internalpolymorphichelpers "github.com/openkruise/kruise-tools/pkg/internal/polymorphichelpers"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||||
|
"k8s.io/cli-runtime/pkg/printers"
|
||||||
|
"k8s.io/cli-runtime/pkg/resource"
|
||||||
|
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||||
|
"k8s.io/kubectl/pkg/scheme"
|
||||||
|
"k8s.io/kubectl/pkg/util/i18n"
|
||||||
|
"k8s.io/kubectl/pkg/util/templates"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
historyLong = templates.LongDesc(i18n.T(`
|
||||||
|
View previous rollout revisions and configurations.`))
|
||||||
|
|
||||||
|
historyExample = templates.Examples(`
|
||||||
|
# View the rollout history of a cloneset
|
||||||
|
kubectl-kruise rollout history cloneset/abc
|
||||||
|
|
||||||
|
# View the details of daemonset revision 3
|
||||||
|
kubectl-kruise rollout history daemonset/abc --revision=3`)
|
||||||
|
)
|
||||||
|
|
||||||
|
// RolloutHistoryOptions holds the options for 'rollout history' sub command
|
||||||
|
type RolloutHistoryOptions struct {
|
||||||
|
PrintFlags *genericclioptions.PrintFlags
|
||||||
|
ToPrinter func(string) (printers.ResourcePrinter, error)
|
||||||
|
|
||||||
|
Revision int64
|
||||||
|
|
||||||
|
Builder func() *resource.Builder
|
||||||
|
Resources []string
|
||||||
|
Namespace string
|
||||||
|
EnforceNamespace bool
|
||||||
|
|
||||||
|
HistoryViewer internalpolymorphichelpers.HistoryViewerFunc
|
||||||
|
RESTClientGetter genericclioptions.RESTClientGetter
|
||||||
|
|
||||||
|
resource.FilenameOptions
|
||||||
|
genericclioptions.IOStreams
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRolloutHistoryOptions returns an initialized RolloutHistoryOptions instance
|
||||||
|
func NewRolloutHistoryOptions(streams genericclioptions.IOStreams) *RolloutHistoryOptions {
|
||||||
|
return &RolloutHistoryOptions{
|
||||||
|
PrintFlags: genericclioptions.NewPrintFlags("").WithTypeSetter(internalapi.GetScheme()),
|
||||||
|
IOStreams: streams,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCmdRolloutHistory returns a Command instance for RolloutHistory sub command
|
||||||
|
func NewCmdRolloutHistory(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command {
|
||||||
|
o := NewRolloutHistoryOptions(streams)
|
||||||
|
|
||||||
|
validArgs := []string{"deployment", "daemonset", "statefulset", "cloneset"}
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "history (TYPE NAME | TYPE/NAME) [flags]",
|
||||||
|
DisableFlagsInUseLine: true,
|
||||||
|
Short: i18n.T("View rollout history"),
|
||||||
|
Long: historyLong,
|
||||||
|
Example: historyExample,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
cmdutil.CheckErr(o.Complete(f, cmd, args))
|
||||||
|
cmdutil.CheckErr(o.Validate())
|
||||||
|
cmdutil.CheckErr(o.Run())
|
||||||
|
},
|
||||||
|
ValidArgs: validArgs,
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Flags().Int64Var(&o.Revision, "revision", o.Revision, "See the details, including podTemplate of the revision specified")
|
||||||
|
|
||||||
|
usage := "identifying the resource to get from a server."
|
||||||
|
cmdutil.AddFilenameOptionFlags(cmd, &o.FilenameOptions, usage)
|
||||||
|
|
||||||
|
o.PrintFlags.AddFlags(cmd)
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// Complete completes al the required options
|
||||||
|
func (o *RolloutHistoryOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error {
|
||||||
|
o.Resources = args
|
||||||
|
|
||||||
|
var err error
|
||||||
|
if o.Namespace, o.EnforceNamespace, err = f.ToRawKubeConfigLoader().Namespace(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
o.ToPrinter = func(operation string) (printers.ResourcePrinter, error) {
|
||||||
|
o.PrintFlags.NamePrintFlags.Operation = operation
|
||||||
|
return o.PrintFlags.ToPrinter()
|
||||||
|
}
|
||||||
|
|
||||||
|
o.HistoryViewer = internalpolymorphichelpers.HistoryViewerFn
|
||||||
|
o.RESTClientGetter = f
|
||||||
|
o.Builder = f.NewBuilder
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate makes sure all the provided values for command-line options are valid
|
||||||
|
func (o *RolloutHistoryOptions) Validate() error {
|
||||||
|
if len(o.Resources) == 0 && cmdutil.IsFilenameSliceEmpty(o.Filenames, o.Kustomize) {
|
||||||
|
return fmt.Errorf("required resource not specified")
|
||||||
|
}
|
||||||
|
if o.Revision < 0 {
|
||||||
|
return fmt.Errorf("revision must be a positive integer: %v", o.Revision)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run performs the execution of 'rollout history' sub command
|
||||||
|
func (o *RolloutHistoryOptions) Run() error {
|
||||||
|
|
||||||
|
r := o.Builder().
|
||||||
|
WithScheme(internalapi.GetScheme(), scheme.Scheme.PrioritizedVersionsAllGroups()...).
|
||||||
|
NamespaceParam(o.Namespace).DefaultNamespace().
|
||||||
|
FilenameParam(o.EnforceNamespace, &o.FilenameOptions).
|
||||||
|
ResourceTypeOrNameArgs(true, o.Resources...).
|
||||||
|
ContinueOnError().
|
||||||
|
Latest().
|
||||||
|
Flatten().
|
||||||
|
Do()
|
||||||
|
if err := r.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.Visit(func(info *resource.Info, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
mapping := info.ResourceMapping()
|
||||||
|
historyViewer, err := o.HistoryViewer(o.RESTClientGetter, mapping)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
historyInfo, err := historyViewer.ViewHistory(info.Namespace, info.Name, o.Revision)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
withRevision := ""
|
||||||
|
if o.Revision > 0 {
|
||||||
|
withRevision = fmt.Sprintf("with revision #%d", o.Revision)
|
||||||
|
}
|
||||||
|
|
||||||
|
printer, err := o.ToPrinter(fmt.Sprintf("%s\n%s", withRevision, historyInfo))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return printer.PrintObj(info.Object, o.Out)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,194 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 The Kruise 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 rollout
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
internalapi "github.com/openkruise/kruise-tools/pkg/api"
|
||||||
|
internalpolymorphichelpers "github.com/openkruise/kruise-tools/pkg/internal/polymorphichelpers"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"k8s.io/apimachinery/pkg/types"
|
||||||
|
utilerrors "k8s.io/apimachinery/pkg/util/errors"
|
||||||
|
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||||
|
"k8s.io/cli-runtime/pkg/printers"
|
||||||
|
"k8s.io/cli-runtime/pkg/resource"
|
||||||
|
"k8s.io/kubectl/pkg/cmd/set"
|
||||||
|
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||||
|
"k8s.io/kubectl/pkg/scheme"
|
||||||
|
"k8s.io/kubectl/pkg/util/i18n"
|
||||||
|
"k8s.io/kubectl/pkg/util/templates"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PauseOptions is the start of the data required to perform the operation. As new fields are added, add them here instead of
|
||||||
|
// referencing the cmd.Flags()
|
||||||
|
type PauseOptions struct {
|
||||||
|
PrintFlags *genericclioptions.PrintFlags
|
||||||
|
ToPrinter func(string) (printers.ResourcePrinter, error)
|
||||||
|
|
||||||
|
Pauser internalpolymorphichelpers.ObjectPauserFunc
|
||||||
|
Builder func() *resource.Builder
|
||||||
|
Namespace string
|
||||||
|
EnforceNamespace bool
|
||||||
|
Resources []string
|
||||||
|
|
||||||
|
resource.FilenameOptions
|
||||||
|
genericclioptions.IOStreams
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
pauseLong = templates.LongDesc(`
|
||||||
|
Mark the provided resource as paused
|
||||||
|
|
||||||
|
Paused resources will not be reconciled by a controller.
|
||||||
|
Use "kubectl rollout resume" to resume a paused resource.
|
||||||
|
Currently deployments, clonesets support being paused.`)
|
||||||
|
|
||||||
|
pauseExample = templates.Examples(`
|
||||||
|
# Mark the nginx deployment as paused. Any current state of
|
||||||
|
# the deployment will continue its function, new updates to the deployment will not
|
||||||
|
# have an effect as long as the deployment is paused.
|
||||||
|
kubectl-kruise rollout pause deployment/nginx`)
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewCmdRolloutPause returns a Command instance for 'rollout pause' sub command
|
||||||
|
func NewCmdRolloutPause(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command {
|
||||||
|
o := &PauseOptions{
|
||||||
|
PrintFlags: genericclioptions.NewPrintFlags("paused").WithTypeSetter(internalapi.GetScheme()),
|
||||||
|
IOStreams: streams,
|
||||||
|
}
|
||||||
|
|
||||||
|
validArgs := []string{"deployment", "cloneset"}
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "pause RESOURCE",
|
||||||
|
DisableFlagsInUseLine: true,
|
||||||
|
Short: i18n.T("Mark the provided resource as paused"),
|
||||||
|
Long: pauseLong,
|
||||||
|
Example: pauseExample,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
cmdutil.CheckErr(o.Complete(f, cmd, args))
|
||||||
|
cmdutil.CheckErr(o.Validate())
|
||||||
|
cmdutil.CheckErr(o.RunPause())
|
||||||
|
},
|
||||||
|
ValidArgs: validArgs,
|
||||||
|
}
|
||||||
|
|
||||||
|
o.PrintFlags.AddFlags(cmd)
|
||||||
|
|
||||||
|
usage := "identifying the resource to get from a server."
|
||||||
|
cmdutil.AddFilenameOptionFlags(cmd, &o.FilenameOptions, usage)
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// Complete completes all the required options
|
||||||
|
func (o *PauseOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error {
|
||||||
|
o.Pauser = internalpolymorphichelpers.ObjectPauserFn
|
||||||
|
|
||||||
|
var err error
|
||||||
|
o.Namespace, o.EnforceNamespace, err = f.ToRawKubeConfigLoader().Namespace()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
o.Resources = args
|
||||||
|
o.Builder = f.NewBuilder
|
||||||
|
|
||||||
|
o.ToPrinter = func(operation string) (printers.ResourcePrinter, error) {
|
||||||
|
o.PrintFlags.NamePrintFlags.Operation = operation
|
||||||
|
return o.PrintFlags.ToPrinter()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *PauseOptions) Validate() error {
|
||||||
|
if len(o.Resources) == 0 && cmdutil.IsFilenameSliceEmpty(o.Filenames, o.Kustomize) {
|
||||||
|
return fmt.Errorf("required resource not specified")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunPause performs the execution of 'rollout pause' sub command
|
||||||
|
func (o *PauseOptions) RunPause() error {
|
||||||
|
r := o.Builder().
|
||||||
|
WithScheme(internalapi.GetScheme(), scheme.Scheme.PrioritizedVersionsAllGroups()...).
|
||||||
|
NamespaceParam(o.Namespace).DefaultNamespace().
|
||||||
|
FilenameParam(o.EnforceNamespace, &o.FilenameOptions).
|
||||||
|
ResourceTypeOrNameArgs(true, o.Resources...).
|
||||||
|
ContinueOnError().
|
||||||
|
Latest().
|
||||||
|
Flatten().
|
||||||
|
Do()
|
||||||
|
if err := r.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var allErrs []error
|
||||||
|
infos, err := r.Infos()
|
||||||
|
if err != nil {
|
||||||
|
// restore previous command behavior where
|
||||||
|
// an error caused by retrieving infos due to
|
||||||
|
// at least a single broken object did not result
|
||||||
|
// in an immediate return, but rather an overall
|
||||||
|
// aggregation of errors.
|
||||||
|
allErrs = append(allErrs, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, patch := range set.CalculatePatches(infos, scheme.DefaultJSONEncoder(), set.PatchFn(o.Pauser)) {
|
||||||
|
info := patch.Info
|
||||||
|
|
||||||
|
if patch.Err != nil {
|
||||||
|
resourceString := info.Mapping.Resource.Resource
|
||||||
|
if len(info.Mapping.Resource.Group) > 0 {
|
||||||
|
resourceString = resourceString + "." + info.Mapping.Resource.Group
|
||||||
|
}
|
||||||
|
allErrs = append(allErrs, fmt.Errorf("error: %s %q %v", resourceString, info.Name, patch.Err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if string(patch.Patch) == "{}" || len(patch.Patch) == 0 {
|
||||||
|
printer, err := o.ToPrinter("already paused")
|
||||||
|
if err != nil {
|
||||||
|
allErrs = append(allErrs, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err = printer.PrintObj(info.Object, o.Out); err != nil {
|
||||||
|
allErrs = append(allErrs, err)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
obj, err := resource.NewHelper(info.Client, info.Mapping).Patch(info.Namespace, info.Name, types.MergePatchType, patch.Patch, nil)
|
||||||
|
if err != nil {
|
||||||
|
allErrs = append(allErrs, fmt.Errorf("failed to patch: %v", err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
info.Refresh(obj, true)
|
||||||
|
printer, err := o.ToPrinter("paused")
|
||||||
|
if err != nil {
|
||||||
|
allErrs = append(allErrs, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err = printer.PrintObj(info.Object, o.Out); err != nil {
|
||||||
|
allErrs = append(allErrs, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return utilerrors.NewAggregate(allErrs)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,192 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 The Kruise 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 rollout
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
internalapi "github.com/openkruise/kruise-tools/pkg/api"
|
||||||
|
internalpolymorphichelpers "github.com/openkruise/kruise-tools/pkg/internal/polymorphichelpers"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"k8s.io/apimachinery/pkg/types"
|
||||||
|
utilerrors "k8s.io/apimachinery/pkg/util/errors"
|
||||||
|
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||||
|
"k8s.io/cli-runtime/pkg/printers"
|
||||||
|
"k8s.io/cli-runtime/pkg/resource"
|
||||||
|
"k8s.io/kubectl/pkg/cmd/set"
|
||||||
|
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||||
|
"k8s.io/kubectl/pkg/scheme"
|
||||||
|
"k8s.io/kubectl/pkg/util/i18n"
|
||||||
|
"k8s.io/kubectl/pkg/util/templates"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RestartOptions is the start of the data required to perform the operation. As new fields are added, add them here instead of
|
||||||
|
// referencing the cmd.Flags()
|
||||||
|
type RestartOptions struct {
|
||||||
|
PrintFlags *genericclioptions.PrintFlags
|
||||||
|
ToPrinter func(string) (printers.ResourcePrinter, error)
|
||||||
|
|
||||||
|
Resources []string
|
||||||
|
|
||||||
|
Builder func() *resource.Builder
|
||||||
|
Restarter internalpolymorphichelpers.ObjectRestarterFunc
|
||||||
|
Namespace string
|
||||||
|
EnforceNamespace bool
|
||||||
|
|
||||||
|
resource.FilenameOptions
|
||||||
|
genericclioptions.IOStreams
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
restartLong = templates.LongDesc(`
|
||||||
|
Restart a resource.
|
||||||
|
|
||||||
|
Resource will be rollout restarted.`)
|
||||||
|
|
||||||
|
restartExample = templates.Examples(`
|
||||||
|
# Restart a deployment
|
||||||
|
kubectl-kruise rollout restart deployment/nginx
|
||||||
|
kubectl-kruise rollout restart cloneset/abc
|
||||||
|
|
||||||
|
# Restart a daemonset
|
||||||
|
kubectl-kruise rollout restart daemonset/abc`)
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewRolloutRestartOptions returns an initialized RestartOptions instance
|
||||||
|
func NewRolloutRestartOptions(streams genericclioptions.IOStreams) *RestartOptions {
|
||||||
|
return &RestartOptions{
|
||||||
|
PrintFlags: genericclioptions.NewPrintFlags("restarted").WithTypeSetter(internalapi.GetScheme()),
|
||||||
|
IOStreams: streams,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCmdRolloutRestart returns a Command instance for 'rollout restart' sub command
|
||||||
|
func NewCmdRolloutRestart(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command {
|
||||||
|
o := NewRolloutRestartOptions(streams)
|
||||||
|
|
||||||
|
validArgs := []string{"deployment", "daemonset", "statefulset"}
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "restart RESOURCE",
|
||||||
|
DisableFlagsInUseLine: true,
|
||||||
|
Short: i18n.T("Restart a resource"),
|
||||||
|
Long: restartLong,
|
||||||
|
Example: restartExample,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
cmdutil.CheckErr(o.Complete(f, cmd, args))
|
||||||
|
cmdutil.CheckErr(o.Validate())
|
||||||
|
cmdutil.CheckErr(o.RunRestart())
|
||||||
|
},
|
||||||
|
ValidArgs: validArgs,
|
||||||
|
}
|
||||||
|
|
||||||
|
usage := "identifying the resource to get from a server."
|
||||||
|
cmdutil.AddFilenameOptionFlags(cmd, &o.FilenameOptions, usage)
|
||||||
|
o.PrintFlags.AddFlags(cmd)
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// Complete completes all the required options
|
||||||
|
func (o *RestartOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error {
|
||||||
|
o.Resources = args
|
||||||
|
|
||||||
|
o.Restarter = internalpolymorphichelpers.ObjectRestarterFn
|
||||||
|
|
||||||
|
var err error
|
||||||
|
o.Namespace, o.EnforceNamespace, err = f.ToRawKubeConfigLoader().Namespace()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
o.ToPrinter = func(operation string) (printers.ResourcePrinter, error) {
|
||||||
|
o.PrintFlags.NamePrintFlags.Operation = operation
|
||||||
|
return o.PrintFlags.ToPrinter()
|
||||||
|
}
|
||||||
|
|
||||||
|
o.Builder = f.NewBuilder
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *RestartOptions) Validate() error {
|
||||||
|
if len(o.Resources) == 0 && cmdutil.IsFilenameSliceEmpty(o.Filenames, o.Kustomize) {
|
||||||
|
return fmt.Errorf("required resource not specified")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunRestart performs the execution of 'rollout restart' sub command
|
||||||
|
func (o RestartOptions) RunRestart() error {
|
||||||
|
r := o.Builder().
|
||||||
|
WithScheme(internalapi.GetScheme(), scheme.Scheme.PrioritizedVersionsAllGroups()...).
|
||||||
|
NamespaceParam(o.Namespace).DefaultNamespace().
|
||||||
|
FilenameParam(o.EnforceNamespace, &o.FilenameOptions).
|
||||||
|
ResourceTypeOrNameArgs(true, o.Resources...).
|
||||||
|
ContinueOnError().
|
||||||
|
Latest().
|
||||||
|
Flatten().
|
||||||
|
Do()
|
||||||
|
if err := r.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
allErrs := []error{}
|
||||||
|
infos, err := r.Infos()
|
||||||
|
if err != nil {
|
||||||
|
// restore previous command behavior where
|
||||||
|
// an error caused by retrieving infos due to
|
||||||
|
// at least a single broken object did not result
|
||||||
|
// in an immediate return, but rather an overall
|
||||||
|
// aggregation of errors.
|
||||||
|
allErrs = append(allErrs, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, patch := range set.CalculatePatches(infos, scheme.DefaultJSONEncoder(), set.PatchFn(o.Restarter)) {
|
||||||
|
info := patch.Info
|
||||||
|
|
||||||
|
if patch.Err != nil {
|
||||||
|
resourceString := info.Mapping.Resource.Resource
|
||||||
|
if len(info.Mapping.Resource.Group) > 0 {
|
||||||
|
resourceString = resourceString + "." + info.Mapping.Resource.Group
|
||||||
|
}
|
||||||
|
allErrs = append(allErrs, fmt.Errorf("error: %s %q %v", resourceString, info.Name, patch.Err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if string(patch.Patch) == "{}" || len(patch.Patch) == 0 {
|
||||||
|
allErrs = append(allErrs, fmt.Errorf("failed to create patch for %v: empty patch", info.Name))
|
||||||
|
}
|
||||||
|
|
||||||
|
obj, err := resource.NewHelper(info.Client, info.Mapping).Patch(info.Namespace, info.Name, types.MergePatchType, patch.Patch, nil)
|
||||||
|
if err != nil {
|
||||||
|
allErrs = append(allErrs, fmt.Errorf("failed to patch: %v", err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
info.Refresh(obj, true)
|
||||||
|
printer, err := o.ToPrinter("restarted")
|
||||||
|
if err != nil {
|
||||||
|
allErrs = append(allErrs, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err = printer.PrintObj(info.Object, o.Out); err != nil {
|
||||||
|
allErrs = append(allErrs, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return utilerrors.NewAggregate(allErrs)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,200 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 The Kruise 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 rollout
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
internalapi "github.com/openkruise/kruise-tools/pkg/api"
|
||||||
|
internalpolymorphichelpers "github.com/openkruise/kruise-tools/pkg/internal/polymorphichelpers"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"k8s.io/apimachinery/pkg/types"
|
||||||
|
utilerrors "k8s.io/apimachinery/pkg/util/errors"
|
||||||
|
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||||
|
"k8s.io/cli-runtime/pkg/printers"
|
||||||
|
"k8s.io/cli-runtime/pkg/resource"
|
||||||
|
"k8s.io/kubectl/pkg/cmd/set"
|
||||||
|
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||||
|
"k8s.io/kubectl/pkg/scheme"
|
||||||
|
"k8s.io/kubectl/pkg/util/i18n"
|
||||||
|
"k8s.io/kubectl/pkg/util/templates"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ResumeOptions is the start of the data required to perform the operation. As new fields are added, add them here instead of
|
||||||
|
// referencing the cmd.Flags()
|
||||||
|
type ResumeOptions struct {
|
||||||
|
PrintFlags *genericclioptions.PrintFlags
|
||||||
|
ToPrinter func(string) (printers.ResourcePrinter, error)
|
||||||
|
|
||||||
|
Resources []string
|
||||||
|
|
||||||
|
Builder func() *resource.Builder
|
||||||
|
Resumer internalpolymorphichelpers.ObjectResumerFunc
|
||||||
|
Namespace string
|
||||||
|
EnforceNamespace bool
|
||||||
|
|
||||||
|
resource.FilenameOptions
|
||||||
|
genericclioptions.IOStreams
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
resumeLong = templates.LongDesc(`
|
||||||
|
Resume a paused resource
|
||||||
|
|
||||||
|
Paused resources will not be reconciled by a controller. By resuming a
|
||||||
|
resource, we allow it to be reconciled again.
|
||||||
|
Currently deployments, cloneset support being resumed.`)
|
||||||
|
|
||||||
|
resumeExample = templates.Examples(`
|
||||||
|
# Resume an already paused deployment
|
||||||
|
|
||||||
|
kubectl-kruise rollout resume cloneset/nginx
|
||||||
|
kubectl-kruise rollout resume deployment/nginx`)
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewRolloutResumeOptions returns an initialized ResumeOptions instance
|
||||||
|
func NewRolloutResumeOptions(streams genericclioptions.IOStreams) *ResumeOptions {
|
||||||
|
return &ResumeOptions{
|
||||||
|
PrintFlags: genericclioptions.NewPrintFlags("resumed").WithTypeSetter(internalapi.GetScheme()),
|
||||||
|
IOStreams: streams,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCmdRolloutResume returns a Command instance for 'rollout resume' sub command
|
||||||
|
func NewCmdRolloutResume(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command {
|
||||||
|
o := NewRolloutResumeOptions(streams)
|
||||||
|
|
||||||
|
validArgs := []string{"deployment", "cloneset"}
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "resume RESOURCE",
|
||||||
|
DisableFlagsInUseLine: true,
|
||||||
|
Short: i18n.T("Resume a paused resource"),
|
||||||
|
Long: resumeLong,
|
||||||
|
Example: resumeExample,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
cmdutil.CheckErr(o.Complete(f, cmd, args))
|
||||||
|
cmdutil.CheckErr(o.Validate())
|
||||||
|
cmdutil.CheckErr(o.RunResume())
|
||||||
|
},
|
||||||
|
ValidArgs: validArgs,
|
||||||
|
}
|
||||||
|
|
||||||
|
usage := "identifying the resource to get from a server."
|
||||||
|
cmdutil.AddFilenameOptionFlags(cmd, &o.FilenameOptions, usage)
|
||||||
|
o.PrintFlags.AddFlags(cmd)
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// Complete completes all the required options
|
||||||
|
func (o *ResumeOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error {
|
||||||
|
o.Resources = args
|
||||||
|
|
||||||
|
o.Resumer = internalpolymorphichelpers.ObjectResumerFn
|
||||||
|
|
||||||
|
var err error
|
||||||
|
o.Namespace, o.EnforceNamespace, err = f.ToRawKubeConfigLoader().Namespace()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
o.ToPrinter = func(operation string) (printers.ResourcePrinter, error) {
|
||||||
|
o.PrintFlags.NamePrintFlags.Operation = operation
|
||||||
|
return o.PrintFlags.ToPrinter()
|
||||||
|
}
|
||||||
|
|
||||||
|
o.Builder = f.NewBuilder
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *ResumeOptions) Validate() error {
|
||||||
|
if len(o.Resources) == 0 && cmdutil.IsFilenameSliceEmpty(o.Filenames, o.Kustomize) {
|
||||||
|
return fmt.Errorf("required resource not specified")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunResume performs the execution of 'rollout resume' sub command
|
||||||
|
func (o ResumeOptions) RunResume() error {
|
||||||
|
r := o.Builder().
|
||||||
|
WithScheme(internalapi.GetScheme(), scheme.Scheme.PrioritizedVersionsAllGroups()...).
|
||||||
|
NamespaceParam(o.Namespace).DefaultNamespace().
|
||||||
|
FilenameParam(o.EnforceNamespace, &o.FilenameOptions).
|
||||||
|
ResourceTypeOrNameArgs(true, o.Resources...).
|
||||||
|
ContinueOnError().
|
||||||
|
Latest().
|
||||||
|
Flatten().
|
||||||
|
Do()
|
||||||
|
if err := r.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
allErrs := []error{}
|
||||||
|
infos, err := r.Infos()
|
||||||
|
if err != nil {
|
||||||
|
// restore previous command behavior where
|
||||||
|
// an error caused by retrieving infos due to
|
||||||
|
// at least a single broken object did not result
|
||||||
|
// in an immediate return, but rather an overall
|
||||||
|
// aggregation of errors.
|
||||||
|
allErrs = append(allErrs, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, patch := range set.CalculatePatches(infos, scheme.DefaultJSONEncoder(), set.PatchFn(o.Resumer)) {
|
||||||
|
info := patch.Info
|
||||||
|
|
||||||
|
if patch.Err != nil {
|
||||||
|
resourceString := info.Mapping.Resource.Resource
|
||||||
|
if len(info.Mapping.Resource.Group) > 0 {
|
||||||
|
resourceString = resourceString + "." + info.Mapping.Resource.Group
|
||||||
|
}
|
||||||
|
allErrs = append(allErrs, fmt.Errorf("error: %s %q %v", resourceString, info.Name, patch.Err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if string(patch.Patch) == "{}" || len(patch.Patch) == 0 {
|
||||||
|
printer, err := o.ToPrinter("already resumed")
|
||||||
|
if err != nil {
|
||||||
|
allErrs = append(allErrs, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err = printer.PrintObj(info.Object, o.Out); err != nil {
|
||||||
|
allErrs = append(allErrs, err)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
obj, err := resource.NewHelper(info.Client, info.Mapping).Patch(info.Namespace, info.Name, types.MergePatchType, patch.Patch, nil)
|
||||||
|
if err != nil {
|
||||||
|
allErrs = append(allErrs, fmt.Errorf("failed to patch: %v", err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
info.Refresh(obj, true)
|
||||||
|
printer, err := o.ToPrinter("resumed")
|
||||||
|
if err != nil {
|
||||||
|
allErrs = append(allErrs, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err = printer.PrintObj(info.Object, o.Out); err != nil {
|
||||||
|
allErrs = append(allErrs, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return utilerrors.NewAggregate(allErrs)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,252 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 The Kruise 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 rollout
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
internalpolymorphichelpers "github.com/openkruise/kruise-tools/pkg/internal/polymorphichelpers"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
internalapi "github.com/openkruise/kruise-tools/pkg/api"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
"k8s.io/apimachinery/pkg/api/meta"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
|
"k8s.io/apimachinery/pkg/fields"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/watch"
|
||||||
|
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||||
|
"k8s.io/cli-runtime/pkg/resource"
|
||||||
|
"k8s.io/client-go/dynamic"
|
||||||
|
"k8s.io/client-go/tools/cache"
|
||||||
|
watchtools "k8s.io/client-go/tools/watch"
|
||||||
|
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||||
|
"k8s.io/kubectl/pkg/scheme"
|
||||||
|
"k8s.io/kubectl/pkg/util/i18n"
|
||||||
|
"k8s.io/kubectl/pkg/util/interrupt"
|
||||||
|
"k8s.io/kubectl/pkg/util/templates"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
statusLong = templates.LongDesc(`
|
||||||
|
Show the status of the rollout.
|
||||||
|
|
||||||
|
By default 'rollout status' will watch the status of the latest rollout
|
||||||
|
until it's done. If you don't want to wait for the rollout to finish then
|
||||||
|
you can use --watch=false. Note that if a new rollout starts in-between, then
|
||||||
|
'rollout status' will continue watching the latest revision. If you want to
|
||||||
|
pin to a specific revision and abort if it is rolled over by another revision,
|
||||||
|
use --revision=N where N is the revision you need to watch for.`)
|
||||||
|
|
||||||
|
statusExample = templates.Examples(`
|
||||||
|
# Watch the rollout status of a cloneset
|
||||||
|
kubectl-kruise rollout status cloneset/nginx`)
|
||||||
|
)
|
||||||
|
|
||||||
|
// RolloutStatusOptions holds the command-line options for 'rollout status' sub command
|
||||||
|
type RolloutStatusOptions struct {
|
||||||
|
PrintFlags *genericclioptions.PrintFlags
|
||||||
|
|
||||||
|
Namespace string
|
||||||
|
EnforceNamespace bool
|
||||||
|
BuilderArgs []string
|
||||||
|
|
||||||
|
Watch bool
|
||||||
|
Revision int64
|
||||||
|
Timeout time.Duration
|
||||||
|
|
||||||
|
StatusViewerFn func(*meta.RESTMapping) (internalpolymorphichelpers.StatusViewer, error)
|
||||||
|
Builder func() *resource.Builder
|
||||||
|
DynamicClient dynamic.Interface
|
||||||
|
|
||||||
|
FilenameOptions *resource.FilenameOptions
|
||||||
|
genericclioptions.IOStreams
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRolloutStatusOptions returns an initialized RolloutStatusOptions instance
|
||||||
|
func NewRolloutStatusOptions(streams genericclioptions.IOStreams) *RolloutStatusOptions {
|
||||||
|
return &RolloutStatusOptions{
|
||||||
|
PrintFlags: genericclioptions.NewPrintFlags("").WithTypeSetter(internalapi.GetScheme()),
|
||||||
|
FilenameOptions: &resource.FilenameOptions{},
|
||||||
|
IOStreams: streams,
|
||||||
|
Watch: true,
|
||||||
|
Timeout: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCmdRolloutStatus returns a Command instance for the 'rollout status' sub command
|
||||||
|
func NewCmdRolloutStatus(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command {
|
||||||
|
o := NewRolloutStatusOptions(streams)
|
||||||
|
|
||||||
|
validArgs := []string{"deployment", "daemonset", "statefulset", "cloneset"}
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "status (TYPE NAME | TYPE/NAME) [flags]",
|
||||||
|
DisableFlagsInUseLine: true,
|
||||||
|
Short: i18n.T("Show the status of the rollout"),
|
||||||
|
Long: statusLong,
|
||||||
|
Example: statusExample,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
cmdutil.CheckErr(o.Complete(f, args))
|
||||||
|
cmdutil.CheckErr(o.Validate())
|
||||||
|
cmdutil.CheckErr(o.Run())
|
||||||
|
},
|
||||||
|
ValidArgs: validArgs,
|
||||||
|
}
|
||||||
|
|
||||||
|
usage := "identifying the resource to get from a server."
|
||||||
|
cmdutil.AddFilenameOptionFlags(cmd, o.FilenameOptions, usage)
|
||||||
|
cmd.Flags().BoolVarP(&o.Watch, "watch", "w", o.Watch, "Watch the status of the rollout until it's done.")
|
||||||
|
cmd.Flags().Int64Var(&o.Revision, "revision", o.Revision, "Pin to a specific revision for showing its status. Defaults to 0 (last revision).")
|
||||||
|
cmd.Flags().DurationVar(&o.Timeout, "timeout", o.Timeout, "The length of time to wait before ending watch, zero means never. Any other values should contain a corresponding time unit (e.g. 1s, 2m, 3h).")
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// Complete completes all the required options
|
||||||
|
func (o *RolloutStatusOptions) Complete(f cmdutil.Factory, args []string) error {
|
||||||
|
o.Builder = f.NewBuilder
|
||||||
|
|
||||||
|
var err error
|
||||||
|
o.Namespace, o.EnforceNamespace, err = f.ToRawKubeConfigLoader().Namespace()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
o.BuilderArgs = args
|
||||||
|
o.StatusViewerFn = internalpolymorphichelpers.StatusViewerFn
|
||||||
|
|
||||||
|
clientConfig, err := f.ToRESTConfig()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
o.DynamicClient, err = dynamic.NewForConfig(clientConfig)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate makes sure all the provided values for command-line options are valid
|
||||||
|
func (o *RolloutStatusOptions) Validate() error {
|
||||||
|
if len(o.BuilderArgs) == 0 && cmdutil.IsFilenameSliceEmpty(o.FilenameOptions.Filenames, o.FilenameOptions.Kustomize) {
|
||||||
|
return fmt.Errorf("required resource not specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.Revision < 0 {
|
||||||
|
return fmt.Errorf("revision must be a positive integer: %v", o.Revision)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run performs the execution of 'rollout status' sub command
|
||||||
|
func (o *RolloutStatusOptions) Run() error {
|
||||||
|
r := o.Builder().
|
||||||
|
WithScheme(internalapi.GetScheme(), scheme.Scheme.PrioritizedVersionsAllGroups()...).
|
||||||
|
NamespaceParam(o.Namespace).DefaultNamespace().
|
||||||
|
FilenameParam(o.EnforceNamespace, o.FilenameOptions).
|
||||||
|
ResourceTypeOrNameArgs(true, o.BuilderArgs...).
|
||||||
|
SingleResourceType().
|
||||||
|
Latest().
|
||||||
|
Do()
|
||||||
|
err := r.Err()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
infos, err := r.Infos()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(infos) != 1 {
|
||||||
|
return fmt.Errorf("rollout status is only supported on individual resources and resource collections - %d resources were found", len(infos))
|
||||||
|
}
|
||||||
|
info := infos[0]
|
||||||
|
mapping := info.ResourceMapping()
|
||||||
|
|
||||||
|
statusViewer, err := o.StatusViewerFn(mapping)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldSelector := fields.OneTermEqualSelector("metadata.name", info.Name).String()
|
||||||
|
lw := &cache.ListWatch{
|
||||||
|
ListFunc: func(options metav1.ListOptions) (runtime.Object, error) {
|
||||||
|
options.FieldSelector = fieldSelector
|
||||||
|
return o.DynamicClient.Resource(info.Mapping.Resource).Namespace(info.Namespace).List(context.TODO(), options)
|
||||||
|
},
|
||||||
|
WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) {
|
||||||
|
options.FieldSelector = fieldSelector
|
||||||
|
return o.DynamicClient.Resource(info.Mapping.Resource).Namespace(info.Namespace).Watch(context.TODO(), options)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
preconditionFunc := func(store cache.Store) (bool, error) {
|
||||||
|
_, exists, err := store.Get(&metav1.ObjectMeta{Namespace: info.Namespace, Name: info.Name})
|
||||||
|
if err != nil {
|
||||||
|
return true, err
|
||||||
|
}
|
||||||
|
if !exists {
|
||||||
|
// We need to make sure we see the object in the cache before we start waiting for events
|
||||||
|
// or we would be waiting for the timeout if such object didn't exist.
|
||||||
|
return true, apierrors.NewNotFound(mapping.Resource.GroupResource(), info.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the rollout isn't done yet, keep watching deployment status
|
||||||
|
ctx, cancel := watchtools.ContextWithOptionalTimeout(context.Background(), o.Timeout)
|
||||||
|
intr := interrupt.New(nil, cancel)
|
||||||
|
return intr.Run(func() error {
|
||||||
|
_, err = watchtools.UntilWithSync(ctx, lw, &unstructured.Unstructured{}, preconditionFunc, func(e watch.Event) (bool, error) {
|
||||||
|
switch t := e.Type; t {
|
||||||
|
case watch.Added, watch.Modified:
|
||||||
|
status, done, err := statusViewer.Status(e.Object.(runtime.Unstructured), o.Revision)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
fmt.Fprintf(o.Out, "%s", status)
|
||||||
|
// Quit waiting if the rollout is done
|
||||||
|
if done {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldWatch := o.Watch
|
||||||
|
if !shouldWatch {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
|
||||||
|
case watch.Deleted:
|
||||||
|
// We need to abort to avoid cases of recreation and not to silently watch the wrong (new) object
|
||||||
|
return true, fmt.Errorf("object has been deleted")
|
||||||
|
|
||||||
|
default:
|
||||||
|
return true, fmt.Errorf("internal error: unexpected event %#v", e)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,190 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 The Kruise Authors.
|
||||||
|
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 rollout
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
internalapi "github.com/openkruise/kruise-tools/pkg/api"
|
||||||
|
internalpolymorphichelpers "github.com/openkruise/kruise-tools/pkg/internal/polymorphichelpers"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||||
|
"k8s.io/cli-runtime/pkg/printers"
|
||||||
|
"k8s.io/cli-runtime/pkg/resource"
|
||||||
|
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||||
|
"k8s.io/kubectl/pkg/scheme"
|
||||||
|
"k8s.io/kubectl/pkg/util/i18n"
|
||||||
|
"k8s.io/kubectl/pkg/util/templates"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UndoOptions is the start of the data required to perform the operation. As new fields are added, add them here instead of
|
||||||
|
// referencing the cmd.Flags()
|
||||||
|
type UndoOptions struct {
|
||||||
|
PrintFlags *genericclioptions.PrintFlags
|
||||||
|
ToPrinter func(string) (printers.ResourcePrinter, error)
|
||||||
|
|
||||||
|
Builder func() *resource.Builder
|
||||||
|
ToRevision int64
|
||||||
|
DryRunStrategy cmdutil.DryRunStrategy
|
||||||
|
DryRunVerifier *resource.DryRunVerifier
|
||||||
|
Resources []string
|
||||||
|
Namespace string
|
||||||
|
EnforceNamespace bool
|
||||||
|
RESTClientGetter genericclioptions.RESTClientGetter
|
||||||
|
|
||||||
|
resource.FilenameOptions
|
||||||
|
genericclioptions.IOStreams
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
undoLong = templates.LongDesc(`
|
||||||
|
Rollback to a previous rollout.`)
|
||||||
|
|
||||||
|
undoExample = templates.Examples(`
|
||||||
|
# Rollback to the previous cloneset
|
||||||
|
kubectl rollout undo cloneset/abc
|
||||||
|
|
||||||
|
# Rollback to daemonset revision 3
|
||||||
|
kubectl rollout undo daemonset/abc --to-revision=3
|
||||||
|
|
||||||
|
# Rollback to the previous deployment with dry-run
|
||||||
|
kubectl rollout undo --dry-run=server deployment/abc`)
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewRolloutUndoOptions returns an initialized UndoOptions instance
|
||||||
|
func NewRolloutUndoOptions(streams genericclioptions.IOStreams) *UndoOptions {
|
||||||
|
return &UndoOptions{
|
||||||
|
PrintFlags: genericclioptions.NewPrintFlags("rolled back").WithTypeSetter(internalapi.GetScheme()),
|
||||||
|
IOStreams: streams,
|
||||||
|
ToRevision: int64(0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCmdRolloutUndo returns a Command instance for the 'rollout undo' sub command
|
||||||
|
func NewCmdRolloutUndo(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command {
|
||||||
|
o := NewRolloutUndoOptions(streams)
|
||||||
|
|
||||||
|
validArgs := []string{"deployment", "daemonset", "statefulset", "cloneset"}
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "undo (TYPE NAME | TYPE/NAME) [flags]",
|
||||||
|
DisableFlagsInUseLine: true,
|
||||||
|
Short: i18n.T("Undo a previous rollout"),
|
||||||
|
Long: undoLong,
|
||||||
|
Example: undoExample,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
cmdutil.CheckErr(o.Complete(f, cmd, args))
|
||||||
|
cmdutil.CheckErr(o.Validate())
|
||||||
|
cmdutil.CheckErr(o.RunUndo())
|
||||||
|
},
|
||||||
|
ValidArgs: validArgs,
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Flags().Int64Var(&o.ToRevision, "to-revision", o.ToRevision, "The revision to rollback to. Default to 0 (last revision).")
|
||||||
|
usage := "identifying the resource to get from a server."
|
||||||
|
cmdutil.AddFilenameOptionFlags(cmd, &o.FilenameOptions, usage)
|
||||||
|
cmdutil.AddDryRunFlag(cmd)
|
||||||
|
o.PrintFlags.AddFlags(cmd)
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// Complete completes al the required options
|
||||||
|
func (o *UndoOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error {
|
||||||
|
o.Resources = args
|
||||||
|
var err error
|
||||||
|
o.DryRunStrategy, err = cmdutil.GetDryRunStrategy(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
dynamicClient, err := f.DynamicClient()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
discoveryClient, err := f.ToDiscoveryClient()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
o.DryRunVerifier = resource.NewDryRunVerifier(dynamicClient, discoveryClient)
|
||||||
|
|
||||||
|
if o.Namespace, o.EnforceNamespace, err = f.ToRawKubeConfigLoader().Namespace(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
o.ToPrinter = func(operation string) (printers.ResourcePrinter, error) {
|
||||||
|
o.PrintFlags.NamePrintFlags.Operation = operation
|
||||||
|
cmdutil.PrintFlagsWithDryRunStrategy(o.PrintFlags, o.DryRunStrategy)
|
||||||
|
return o.PrintFlags.ToPrinter()
|
||||||
|
}
|
||||||
|
|
||||||
|
o.RESTClientGetter = f
|
||||||
|
o.Builder = f.NewBuilder
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *UndoOptions) Validate() error {
|
||||||
|
if len(o.Resources) == 0 && cmdutil.IsFilenameSliceEmpty(o.Filenames, o.Kustomize) {
|
||||||
|
return fmt.Errorf("required resource not specified")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunUndo performs the execution of 'rollout undo' sub command
|
||||||
|
func (o *UndoOptions) RunUndo() error {
|
||||||
|
r := o.Builder().
|
||||||
|
WithScheme(internalapi.GetScheme(), scheme.Scheme.PrioritizedVersionsAllGroups()...).
|
||||||
|
NamespaceParam(o.Namespace).DefaultNamespace().
|
||||||
|
FilenameParam(o.EnforceNamespace, &o.FilenameOptions).
|
||||||
|
ResourceTypeOrNameArgs(true, o.Resources...).
|
||||||
|
ContinueOnError().
|
||||||
|
Latest().
|
||||||
|
Flatten().
|
||||||
|
Do()
|
||||||
|
if err := r.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err := r.Visit(func(info *resource.Info, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
rollbacker, err := internalpolymorphichelpers.RollbackerFn(o.RESTClientGetter, info.ResourceMapping())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.DryRunStrategy == cmdutil.DryRunServer {
|
||||||
|
if err := o.DryRunVerifier.HasSupport(info.Mapping.GroupVersionKind); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result, err := rollbacker.Rollback(info.Object, nil, o.ToRevision, o.DryRunStrategy)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
printer, err := o.ToPrinter(result)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return printer.PrintObj(info.Object, o.Out)
|
||||||
|
})
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
package fetcher
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
kruiseappsv1alpha1 "github.com/openkruise/kruise-api/apps/v1alpha1"
|
||||||
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/types"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetResourceInCache(ns, name string, obj runtime.Object, cl client.Reader) (bool, error) {
|
||||||
|
err := cl.Get(context.TODO(), types.NamespacedName{Namespace: ns, Name: name}, obj)
|
||||||
|
if err != nil {
|
||||||
|
if apierrors.IsNotFound(err) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ListResourceInCache(ns string, obj runtime.Object, cl client.Client) error {
|
||||||
|
return cl.List(context.TODO(), obj, client.InNamespace(ns))
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetCloneSetInCache(ns, name string, cl client.Reader) (*kruiseappsv1alpha1.CloneSet, bool, error) {
|
||||||
|
cs := &kruiseappsv1alpha1.CloneSet{}
|
||||||
|
found, err := GetResourceInCache(ns, name, cs, cl)
|
||||||
|
if err != nil || !found {
|
||||||
|
cs = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return cs, found, err
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 The Kruise Authors.
|
||||||
|
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)
|
||||||
|
VisitCloneSet(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)
|
||||||
|
case elem.GroupMatch("apps.kruise.io") && elem.Kind == "CloneSet":
|
||||||
|
visitor.VisitCloneSet(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,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,46 @@
|
||||||
|
/*
|
||||||
|
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"
|
||||||
|
kruiseappsv1alpha1 "github.com/openkruise/kruise-api/apps/v1alpha1"
|
||||||
|
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(),
|
||||||
|
kruiseappsv1alpha1.SchemeGroupVersion.WithKind("CloneSet").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,491 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 The Kruise Authors.
|
||||||
|
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"
|
||||||
|
|
||||||
|
kruiseappsv1alpha1 "github.com/openkruise/kruise-api/apps/v1alpha1"
|
||||||
|
internalapi "github.com/openkruise/kruise-tools/pkg/api"
|
||||||
|
"github.com/openkruise/kruise-tools/pkg/fetcher"
|
||||||
|
internalapps "github.com/openkruise/kruise-tools/pkg/internal/apps"
|
||||||
|
|
||||||
|
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/klog"
|
||||||
|
"k8s.io/kubectl/pkg/describe"
|
||||||
|
deploymentutil "k8s.io/kubectl/pkg/util/deployment"
|
||||||
|
sliceutil "k8s.io/kubectl/pkg/util/slice"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
c client.Reader
|
||||||
|
result HistoryViewer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *HistoryVisitor) VisitDeployment(elem internalapps.GroupKindElement) {
|
||||||
|
v.result = &DeploymentHistoryViewer{v.clientset}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *HistoryVisitor) VisitStatefulSet(kind internalapps.GroupKindElement) {
|
||||||
|
v.result = &StatefulSetHistoryViewer{v.clientset}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *HistoryVisitor) VisitDaemonSet(kind internalapps.GroupKindElement) {
|
||||||
|
v.result = &DaemonSetHistoryViewer{v.clientset}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *HistoryVisitor) VisitJob(kind internalapps.GroupKindElement) {}
|
||||||
|
func (v *HistoryVisitor) VisitPod(kind internalapps.GroupKindElement) {}
|
||||||
|
func (v *HistoryVisitor) VisitReplicaSet(kind internalapps.GroupKindElement) {}
|
||||||
|
func (v *HistoryVisitor) VisitReplicationController(kind internalapps.GroupKindElement) {}
|
||||||
|
func (v *HistoryVisitor) VisitCronJob(kind internalapps.GroupKindElement) {}
|
||||||
|
|
||||||
|
// HistoryViewerFor returns an implementation of HistoryViewer interface for the given schema kind
|
||||||
|
func HistoryViewerFor(kind schema.GroupKind, c kubernetes.Interface) (HistoryViewer, error) {
|
||||||
|
elem := internalapps.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
|
||||||
|
}
|
||||||
|
|
||||||
|
type CloneSetHistoryViewer struct {
|
||||||
|
c client.Reader
|
||||||
|
k kubernetes.Interface
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *HistoryVisitor) VisitCloneSet(kind internalapps.GroupKindElement) {
|
||||||
|
mgr := internalapi.NewManager()
|
||||||
|
v.c = mgr.GetAPIReader()
|
||||||
|
v.result = &CloneSetHistoryViewer{v.c, v.clientset}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO impl ViewHistory func for CloneSet
|
||||||
|
func (h *CloneSetHistoryViewer) ViewHistory(namespace, name string, revision int64) (string, error) {
|
||||||
|
|
||||||
|
cs, history, err := clonesetHistory(h.k.AppsV1(), h.c, namespace, name)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return printHistory(history, revision, func(history *appsv1.ControllerRevision) (*corev1.PodTemplateSpec, error) {
|
||||||
|
stsOfHistory, err := applyCloneSetHistory(cs, history)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &stsOfHistory.Spec.Template, err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
func clonesetHistory(
|
||||||
|
apps clientappsv1.AppsV1Interface, cr client.Reader,
|
||||||
|
namespace, name string) (*kruiseappsv1alpha1.CloneSet, []*appsv1.ControllerRevision, error) {
|
||||||
|
cs, found, err := fetcher.GetCloneSetInCache(namespace, name, cr)
|
||||||
|
if err != nil || !found {
|
||||||
|
klog.Error(err)
|
||||||
|
return nil, nil, fmt.Errorf("failed to retrieve CloneSet %s: %s", name, err.Error())
|
||||||
|
}
|
||||||
|
selector, err := metav1.LabelSelectorAsSelector(cs.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(cs)
|
||||||
|
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 cs, 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// applyCloneSetHistory returns a specific revision of CloneSet by applying the given history to a copy of the given CloneSet
|
||||||
|
func applyCloneSetHistory(cs *kruiseappsv1alpha1.CloneSet,
|
||||||
|
history *appsv1.ControllerRevision) (*kruiseappsv1alpha1.CloneSet, error) {
|
||||||
|
csBytes, err := json.Marshal(cs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
patched, err := strategicpatch.StrategicMergePatch(csBytes, history.Data.Raw, cs)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result := &kruiseappsv1alpha1.CloneSet{}
|
||||||
|
|
||||||
|
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,214 @@
|
||||||
|
/*
|
||||||
|
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/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.
|
||||||
|
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:
|
||||||
|
// in case the "kubectl.kubernetes.io/default-logs-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 && len(opts.Container) == 0 && len(annotations[defaultLogsContainerAnnotationName]) > 0 {
|
||||||
|
containerName := annotations[defaultLogsContainerAnnotationName]
|
||||||
|
if exists, _ := findContainerByName(t, containerName); exists != nil {
|
||||||
|
opts.Container = containerName
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(os.Stderr, "Default container name %q not found in a pod\n", containerName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 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 {
|
||||||
|
var containerName string
|
||||||
|
if opts == nil || len(opts.Container) == 0 {
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
containerName = t.Spec.Containers[0].Name
|
||||||
|
} else {
|
||||||
|
containerName = opts.Container
|
||||||
|
}
|
||||||
|
|
||||||
|
container, fieldPath := findContainerByName(t, containerName)
|
||||||
|
if container == nil {
|
||||||
|
return nil, fmt.Errorf("container %s is not valid for pod %s", opts.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, opts)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// findContainerByName searches for a container by name amongst all containers in a pod.
|
||||||
|
// Returns a pointer to a container and a field path.
|
||||||
|
func findContainerByName(pod *corev1.Pod, name string) (container *corev1.Container, fieldPath string) {
|
||||||
|
for _, c := range pod.Spec.InitContainers {
|
||||||
|
if c.Name == name {
|
||||||
|
return &c, fmt.Sprintf("spec.initContainers{%s}", c.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, c := range pod.Spec.Containers {
|
||||||
|
if c.Name == name {
|
||||||
|
return &c, fmt.Sprintf("spec.containers{%s}", c.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, c := range pod.Spec.EphemeralContainers {
|
||||||
|
if c.Name == name {
|
||||||
|
containerCommon := corev1.Container(c.EphemeralContainerCommon)
|
||||||
|
return &containerCommon, fmt.Sprintf("spec.ephemeralContainers{%s}", containerCommon.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
@ -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,73 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 The Kruise Authors.
|
||||||
|
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"
|
||||||
|
kruiseappsv1alpha1 "github.com/openkruise/kruise-api/apps/v1alpha1"
|
||||||
|
|
||||||
|
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)
|
||||||
|
case *kruiseappsv1alpha1.CloneSet:
|
||||||
|
if obj.Spec.UpdateStrategy.Paused {
|
||||||
|
return nil, errors.New("is already paused")
|
||||||
|
}
|
||||||
|
obj.Spec.UpdateStrategy.Paused = true
|
||||||
|
return runtime.Encode(scheme.Codecs.LegacyCodec(kruiseappsv1alpha1.SchemeGroupVersion), obj)
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("pausing is not supported")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,130 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 The Kruise Authors.
|
||||||
|
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"
|
||||||
|
kruiseappsv1alpha1 "github.com/openkruise/kruise-api/apps/v1alpha1"
|
||||||
|
"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)
|
||||||
|
case *kruiseappsv1alpha1.CloneSet:
|
||||||
|
if obj.Spec.Template.ObjectMeta.Annotations == nil {
|
||||||
|
obj.Spec.Template.ObjectMeta.Annotations = make(map[string]string)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO is InPlaceOnly UpdateStrategy spport rollout restart?
|
||||||
|
|
||||||
|
obj.Spec.Template.ObjectMeta.Annotations["kubectl.kruise.io/restartedAt"] = time.Now().Format(time.RFC3339)
|
||||||
|
return runtime.Encode(scheme.Codecs.LegacyCodec(kruiseappsv1alpha1.SchemeGroupVersion), obj)
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("restarting is not supported")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 The Kruise Authors.
|
||||||
|
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"
|
||||||
|
kruiseappsv1alpha1 "github.com/openkruise/kruise-api/apps/v1alpha1"
|
||||||
|
|
||||||
|
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)
|
||||||
|
case *kruiseappsv1alpha1.CloneSet:
|
||||||
|
if !obj.Spec.UpdateStrategy.Paused {
|
||||||
|
return nil, errors.New("is not paused")
|
||||||
|
}
|
||||||
|
obj.Spec.UpdateStrategy.Paused = false
|
||||||
|
return runtime.Encode(scheme.Codecs.LegacyCodec(kruiseappsv1alpha1.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
|
||||||
|
}
|
||||||
|
|
@ -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,625 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 The Kruise Authors.
|
||||||
|
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"
|
||||||
|
kruiseappsv1alpha1 "github.com/openkruise/kruise-api/apps/v1alpha1"
|
||||||
|
internalapi "github.com/openkruise/kruise-tools/pkg/api"
|
||||||
|
internalapps "github.com/openkruise/kruise-tools/pkg/internal/apps"
|
||||||
|
"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"
|
||||||
|
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||||
|
"k8s.io/kubectl/pkg/scheme"
|
||||||
|
deploymentutil "k8s.io/kubectl/pkg/util/deployment"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
cr client.Reader
|
||||||
|
result Rollbacker
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *RollbackVisitor) VisitDeployment(elem internalapps.GroupKindElement) {
|
||||||
|
v.result = &DeploymentRollbacker{v.clientset}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *RollbackVisitor) VisitStatefulSet(kind internalapps.GroupKindElement) {
|
||||||
|
v.result = &StatefulSetRollbacker{v.clientset}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *RollbackVisitor) VisitDaemonSet(kind internalapps.GroupKindElement) {
|
||||||
|
v.result = &DaemonSetRollbacker{v.clientset}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *RollbackVisitor) VisitCloneSet(kind internalapps.GroupKindElement) {
|
||||||
|
mgr := internalapi.NewManager()
|
||||||
|
cr := mgr.GetAPIReader()
|
||||||
|
c := mgr.GetClient()
|
||||||
|
v.result = &CloneSetRollbacker{cr: cr, c: c, k: v.clientset}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *RollbackVisitor) VisitJob(kind internalapps.GroupKindElement) {}
|
||||||
|
func (v *RollbackVisitor) VisitPod(kind internalapps.GroupKindElement) {}
|
||||||
|
func (v *RollbackVisitor) VisitReplicaSet(kind internalapps.GroupKindElement) {}
|
||||||
|
func (v *RollbackVisitor) VisitReplicationController(kind internalapps.GroupKindElement) {}
|
||||||
|
func (v *RollbackVisitor) VisitCronJob(kind internalapps.GroupKindElement) {}
|
||||||
|
|
||||||
|
// RollbackerFor returns an implementation of Rollbacker interface for the given schema kind
|
||||||
|
func RollbackerFor(kind schema.GroupKind, c kubernetes.Interface) (Rollbacker, error) {
|
||||||
|
elem := internalapps.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
|
||||||
|
}
|
||||||
|
|
||||||
|
type CloneSetRollbacker struct {
|
||||||
|
cr client.Reader
|
||||||
|
c client.Client
|
||||||
|
k kubernetes.Interface
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *CloneSetRollbacker) 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())
|
||||||
|
}
|
||||||
|
cs, history, err := clonesetHistory(r.k.AppsV1(), r.cr, 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 := applyCloneSetRevision(cs, toHistory)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return printPodTemplate(&appliedSS.Spec.Template)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip if the revision already matches current CloneSet
|
||||||
|
done, err := cloneSetMatch(cs, toHistory)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if done {
|
||||||
|
return fmt.Sprintf("%s (current template already matches revision %d)", rollbackSkipped, toRevision), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore revision
|
||||||
|
if dryRunStrategy == cmdutil.DryRunServer {
|
||||||
|
if err = r.c.Patch(context.TODO(), cs, client.RawPatch(types.MergePatchType,
|
||||||
|
toHistory.Data.Raw), client.DryRunAll); err != nil {
|
||||||
|
return "", fmt.Errorf("failed restoring revision %d: %v", toRevision, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err = r.c.Patch(context.TODO(), cs, client.RawPatch(types.MergePatchType,
|
||||||
|
toHistory.Data.Raw)); 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
|
||||||
|
}
|
||||||
|
|
||||||
|
var kruiseAppsCodec = scheme.Codecs.LegacyCodec(kruiseappsv1alpha1.SchemeGroupVersion)
|
||||||
|
|
||||||
|
// applyCloneSetRevision returns a new CloneSet constructed by restoring the state in revision to set. If the returned error
|
||||||
|
// is nil, the returned CloneSet is valid.
|
||||||
|
func applyCloneSetRevision(cs *kruiseappsv1alpha1.CloneSet,
|
||||||
|
revision *appsv1.ControllerRevision) (*kruiseappsv1alpha1.CloneSet, error) {
|
||||||
|
patched, err := strategicpatch.StrategicMergePatch([]byte(runtime.EncodeOrDie(kruiseAppsCodec, cs)), revision.Data.Raw, cs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result := &kruiseappsv1alpha1.CloneSet{}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// cloneSetMatch check if the given CloneSet's template matches the template stored in the given history.
|
||||||
|
func cloneSetMatch(cs *kruiseappsv1alpha1.CloneSet, history *appsv1.ControllerRevision) (bool, error) {
|
||||||
|
patch, err := getCloneSetPatch(cs)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// getCloneSetPatch returns a strategic merge patch that can be applied to restore a CloneSet to a
|
||||||
|
// previous version.
|
||||||
|
func getCloneSetPatch(cs *kruiseappsv1alpha1.CloneSet) ([]byte, error) {
|
||||||
|
str, err := runtime.Encode(kruiseAppsCodec, cs)
|
||||||
|
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,189 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 The Kruise Authors.
|
||||||
|
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"
|
||||||
|
kruiseappsv1alpha1 "github.com/openkruise/kruise-api/apps/v1alpha1"
|
||||||
|
|
||||||
|
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
|
||||||
|
case kruiseappsv1alpha1.SchemeGroupVersion.WithKind("CloneSet").GroupKind():
|
||||||
|
return &CloneSetStatusViewer{}, 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{}
|
||||||
|
|
||||||
|
// CloneSetViewer implements the StatusViewer interface
|
||||||
|
type CloneSetStatusViewer 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
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status returns a message describing cloneset status, and a bool value indicating if the status is considered done.
|
||||||
|
func (s *CloneSetStatusViewer) Status(obj runtime.Unstructured, revision int64) (string, bool, error) {
|
||||||
|
cs := &kruiseappsv1alpha1.CloneSet{}
|
||||||
|
err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.UnstructuredContent(), cs)
|
||||||
|
if err != nil {
|
||||||
|
return "", false, fmt.Errorf("failed to convert %T to %T: %v", obj, cs, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// check InPlaceOnly and InPlacePossible UpdateStrategy
|
||||||
|
if cs.Spec.UpdateStrategy.Type == kruiseappsv1alpha1.InPlaceOnlyCloneSetUpdateStrategyType ||
|
||||||
|
cs.Spec.UpdateStrategy.Type == kruiseappsv1alpha1.InPlaceIfPossibleCloneSetUpdateStrategyType {
|
||||||
|
if cs.Spec.Replicas != nil && cs.Spec.UpdateStrategy.Partition != nil {
|
||||||
|
if cs.Status.UpdatedReplicas < (*cs.Spec.Replicas - cs.Spec.UpdateStrategy.Partition.IntVal) {
|
||||||
|
return fmt.Sprintf("Waiting for partitioned roll out to finish: %d out of %d new pods have been updated...\n",
|
||||||
|
cs.Status.UpdatedReplicas, *cs.Spec.Replicas-cs.Spec.UpdateStrategy.Partition.IntVal), false, nil
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if cs.Status.ObservedGeneration == 0 || cs.Generation > cs.Status.ObservedGeneration {
|
||||||
|
return "Waiting for CloneSet spec update to be observed...\n", false, nil
|
||||||
|
}
|
||||||
|
if cs.Spec.Replicas != nil && cs.Status.ReadyReplicas < *cs.Spec.Replicas {
|
||||||
|
return fmt.Sprintf("Waiting for %d pods to be ready...\n", *cs.Spec.Replicas-cs.Status.ReadyReplicas), false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("CloneSet rolling update complete %d pods at revision %s...\n", cs.Status.AvailableReplicas, cs.Status.UpdateRevision), 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,91 @@
|
||||||
|
/*
|
||||||
|
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"
|
||||||
|
batchv2alpha1 "k8s.io/api/batch/v2alpha1"
|
||||||
|
"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 *batchv2alpha1.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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -190,7 +190,7 @@ func (c *control) Query(ID types.UID) (migration.Result, error) {
|
||||||
|
|
||||||
func (c *control) addEventHandler(gvk schema.GroupVersionKind) error {
|
func (c *control) addEventHandler(gvk schema.GroupVersionKind) error {
|
||||||
if _, ok := c.handledGVKs[gvk]; !ok {
|
if _, ok := c.handledGVKs[gvk]; !ok {
|
||||||
informer, err := c.cache.GetInformerForKind(gvk)
|
informer, err := c.cache.GetInformerForKind(context.Background(), gvk)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get informer for %v: %v", gvk, err)
|
return fmt.Errorf("failed to get informer for %v: %v", gvk, err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if [[ -n ${GIT_COMMIT-} ]] || GIT_COMMIT=$(git rev-parse "HEAD^{commit}" 2>/dev/null); then
|
||||||
|
if [[ -z ${GIT_TREE_STATE-} ]]; then
|
||||||
|
# Check if the tree is dirty. default to dirty
|
||||||
|
if git_status=$(git status --porcelain 2>/dev/null) && [[ -z ${git_status} ]]; then
|
||||||
|
GIT_TREE_STATE="clean"
|
||||||
|
else
|
||||||
|
GIT_TREE_STATE="dirty"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Use git describe to find the version based on tags.
|
||||||
|
if [[ -n ${GIT_VERSION-} ]] || GIT_VERSION=$(git describe --tags --abbrev=14 "${GIT_COMMIT}^{commit}" 2>/dev/null); then
|
||||||
|
# This translates the "git describe" to an actual semver.org
|
||||||
|
# compatible semantic version that looks something like this:
|
||||||
|
# v1.0.0-beta.0.10+4c183422345d8f
|
||||||
|
#
|
||||||
|
# downstream consumers are expecting it there.
|
||||||
|
DASHES_IN_VERSION=$(echo "${GIT_VERSION}" | sed "s/[^-]//g")
|
||||||
|
if [[ "${DASHES_IN_VERSION}" == "---" ]] ; then
|
||||||
|
# We have distance to subversion (v1.1.0-subversion-1-gCommitHash)
|
||||||
|
GIT_VERSION=$(echo "${GIT_VERSION}" | sed "s/-\([0-9]\{1,\}\)-g\([0-9a-f]\{14\}\)$/.\1\+\2/")
|
||||||
|
elif [[ "${DASHES_IN_VERSION}" == "--" ]] ; then
|
||||||
|
# We have distance to base tag (v1.1.0-1-gCommitHash)
|
||||||
|
GIT_VERSION=$(echo "${GIT_VERSION}" | sed "s/-g\([0-9a-f]\{14\}\)$/+\1/")
|
||||||
|
fi
|
||||||
|
if [[ "${GIT_TREE_STATE}" == "dirty" ]]; then
|
||||||
|
# git describe --dirty only considers changes to existing files, but
|
||||||
|
# that is problematic since new untracked .go files affect the build,
|
||||||
|
# so use our idea of "dirty" from git status instead.
|
||||||
|
GIT_VERSION+="-dirty"
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
# If GIT_VERSION is not a valid Semantic Version, then refuse to build.
|
||||||
|
if ! [[ "${GIT_VERSION}" =~ ^v([0-9]+)\.([0-9]+)(\.[0-9]+)?(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$ ]]; then
|
||||||
|
echo "GIT_VERSION should be a valid Semantic Version. Current value: ${GIT_VERSION}"
|
||||||
|
echo "Please see more details here: https://semver.org"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "-X 'github.com/github.com/openkruise/kruise-tools/version.gitVersion=${GIT_VERSION}'"
|
||||||
Loading…
Reference in New Issue