refactor: upgrade deploy image if client version is incompatibility with image tag (#470)

This commit is contained in:
naison
2025-03-14 20:52:22 +08:00
committed by GitHub
parent a70ce62762
commit 77570575ca
9 changed files with 704 additions and 252 deletions

View File

@@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"fmt"
"net"
"net/url"
"reflect"
@@ -15,8 +16,6 @@ import (
"time"
"github.com/containernetworking/cni/pkg/types"
"github.com/distribution/reference"
goversion "github.com/hashicorp/go-version"
"github.com/libp2p/go-netroute"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
@@ -29,6 +28,7 @@ import (
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/labels"
pkgruntime "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
pkgtypes "k8s.io/apimachinery/pkg/types"
utilnet "k8s.io/apimachinery/pkg/util/net"
"k8s.io/apimachinery/pkg/util/runtime"
@@ -41,6 +41,7 @@ import (
"k8s.io/kubectl/pkg/cmd/set"
cmdutil "k8s.io/kubectl/pkg/cmd/util"
"k8s.io/kubectl/pkg/polymorphichelpers"
"k8s.io/kubectl/pkg/scale"
"k8s.io/kubectl/pkg/scheme"
"k8s.io/kubectl/pkg/util/podutils"
"k8s.io/utils/pointer"
@@ -918,29 +919,45 @@ func (c *ConnectOptions) upgradeDeploy(ctx context.Context) error {
return fmt.Errorf("can not found any container in deploy %s", deploy.Name)
}
clientVer := config.Version
clientImg := config.Image
serverImg := deploy.Spec.Template.Spec.Containers[0].Image
if clientImg == serverImg {
isNeedUpgrade, err := util.IsNewer(clientVer, clientImg, serverImg)
if !isNeedUpgrade {
return nil
}
isNeedUpgrade, _ := newer(config.Version, clientImg, serverImg)
if deploy.Status.ReadyReplicas > 0 && !isNeedUpgrade {
return nil
if err != nil {
return err
}
log.Infof("Set image %s --> %s...", serverImg, clientImg)
r := c.factory.NewBuilder().
err = upgradeDeploySpec(ctx, c.factory, c.Namespace, deploy.Name, clientImg)
if err != nil {
return err
}
// because use webhook(kubevpn-traffic-manager container webhook) to assign ip,
// if create new pod use old webhook, ip will still change to old CIDR.
// so after patched, check again if env is newer or not,
// if env is still old, needs to re-patch using new webhook
err = restartDeploy(ctx, c.factory, c.clientset, c.Namespace, deploy.Name)
if err != nil {
return err
}
return nil
}
func upgradeDeploySpec(ctx context.Context, f cmdutil.Factory, ns, name string, targetImage string) error {
r := f.NewBuilder().
WithScheme(scheme.Scheme, scheme.Scheme.PrioritizedVersionsAllGroups()...).
NamespaceParam(c.Namespace).DefaultNamespace().
ResourceNames("deployments", deploy.Name).
NamespaceParam(ns).DefaultNamespace().
ResourceNames("deployments", name).
ContinueOnError().
Latest().
Flatten().
Do()
if err = r.Err(); err != nil {
if err := r.Err(); err != nil {
return err
}
infos, err := r.Infos()
@@ -966,7 +983,7 @@ func (c *ConnectOptions) upgradeDeploy(ctx context.Context) error {
patches := set.CalculatePatches(infos, scheme.DefaultJSONEncoder(), func(obj pkgruntime.Object) ([]byte, error) {
_, err = polymorphichelpers.UpdatePodSpecForObjectFn(obj, func(spec *v1.PodSpec) error {
for i := range spec.Containers {
spec.Containers[i].Image = clientImg
spec.Containers[i].Image = targetImage
// update tun cidr for vpn
if spec.Containers[i].Name == config.ContainerSidecarVPN {
@@ -1027,7 +1044,7 @@ func (c *ConnectOptions) upgradeDeploy(ctx context.Context) error {
log.Errorf("Failed to patch image update to pod template: %v", err)
return err
}
err = util.RolloutStatus(ctx, c.factory, c.Namespace, fmt.Sprintf("%s/%s", p.Info.Mapping.Resource.GroupResource().String(), p.Info.Name), time.Minute*60)
err = util.RolloutStatus(ctx, f, ns, fmt.Sprintf("%s/%s", p.Info.Mapping.Resource.GroupResource().String(), p.Info.Name), time.Minute*60)
if err != nil {
return err
}
@@ -1035,64 +1052,57 @@ func (c *ConnectOptions) upgradeDeploy(ctx context.Context) error {
return nil
}
func newer(clientCliVersionStr, clientImgStr, serverImgStr string) (bool, error) {
clientImg, err := reference.ParseNormalizedNamed(clientImgStr)
func restartDeploy(ctx context.Context, f cmdutil.Factory, clientset *kubernetes.Clientset, ns, name string) error {
label := fields.OneTermEqualSelector("app", config.ConfigMapPodTrafficManager).String()
list, err := util.GetRunningPodList(ctx, clientset, ns, label)
if err != nil {
return false, err
return err
}
serverImg, err := reference.ParseNormalizedNamed(serverImgStr)
pod := list[0]
container, _ := util.FindContainerByName(&pod, config.ContainerSidecarVPN)
if container == nil {
return nil
}
envs := map[string]string{
"CIDR4": config.CIDR.String(),
"CIDR6": config.CIDR6.String(),
config.EnvInboundPodTunIPv4: (&net.IPNet{IP: config.RouterIP, Mask: config.CIDR.Mask}).String(),
config.EnvInboundPodTunIPv6: (&net.IPNet{IP: config.RouterIP6, Mask: config.CIDR6.Mask}).String(),
}
var mismatch bool
for _, existing := range container.Env {
if envs[existing.Name] != existing.Value {
mismatch = true
break
}
}
if !mismatch {
return nil
}
scalesGetter, err := cmdutil.ScaleClientFn(f)
if err != nil {
return false, err
return err
}
clientImgTag, ok := clientImg.(reference.NamedTagged)
if !ok {
return false, fmt.Errorf("can not convert client image")
scaler := scale.NewScaler(scalesGetter)
retry := scale.NewRetryParams(1*time.Second, 5*time.Minute)
waitForReplicas := scale.NewRetryParams(1*time.Second, 1)
gvr := schema.GroupVersionResource{
Group: "apps",
Version: "v1",
Resource: "deployments",
}
// 1. if client image version is same as client cli version, does not need to upgrade
// kubevpn connect --image=ghcr.io/kubenetworks/kubevpn:v2.3.0 or --kubevpnconfig
// the kubevpn version is v2.3.1
if clientImgTag.Tag() != clientCliVersionStr {
// TODO: is it necessary to exit the process?
log.Warnf("\033[33mCurrent kubevpn cli version is %s, please use the same version of kubevpn image with flag \"--image\"\033[0m", clientCliVersionStr)
return false, nil
}
serverImgTag, ok := serverImg.(reference.NamedTagged)
if !ok {
return false, fmt.Errorf("can not convert server image")
}
// 2. if server image version is same as client cli version, does not need to upgrade
if serverImgTag.Tag() == clientCliVersionStr {
return false, nil
}
// 3. check custom server image registry
// if custom server image domain is not same as config.GHCR_IMAGE_REGISTRY
// and not same as config.DOCKER_IMAGE_REGISTRY
// and not same as client images(may be used --image)
// does not need to upgrade
serverImgDomain := reference.Domain(serverImg)
clientImgDomain := reference.Domain(clientImg)
if serverImgDomain != config.GHCR_IMAGE_REGISTRY && serverImgDomain != config.DOCKER_IMAGE_REGISTRY && serverImgDomain != clientImgDomain {
newImageStr := fmt.Sprintf("%s:%s", serverImg.Name(), clientCliVersionStr)
log.Warnf("\033[33mCurrent kubevpn cli version is %s, please manually upgrade 'kubevpn-traffic-manager' control plane pod container image to %s\033[0m", clientCliVersionStr, newImageStr)
return false, nil
}
serverImgVersion, err := goversion.NewVersion(serverImgTag.Tag())
err = scaler.Scale(ns, name, 0, nil, retry, waitForReplicas, gvr, false)
if err != nil {
return false, err
return err
}
clientImgVersion, err := goversion.NewVersion(clientImgTag.Tag())
err = scaler.Scale(ns, name, 1, nil, retry, waitForReplicas, gvr, false)
if err != nil {
return false, err
return err
}
// 4. check client image version is greater than server image version
return clientImgVersion.GreaterThan(serverImgVersion), nil
err = util.RolloutStatus(ctx, f, ns, fmt.Sprintf("%s/%s", "deployments", name), time.Minute*60)
return err
}
func (c *ConnectOptions) Equal(a *ConnectOptions) bool {

View File

@@ -149,178 +149,3 @@ func TestRemoveCIDRsContainingIPs(t *testing.T) {
})
}
}
func Test_newer(t *testing.T) {
type args struct {
clientVersionStr string
clientImgStr string
serverImgStr string
}
tests := []struct {
name string
args args
want bool
wantErr bool
}{
// client version: v1.2.1
// client image: ghcr.io/kubenetworks/kubevpn:v1.2.1
// server image: naison/kubevpn:v1.0.0
{
name: "Valid case - client(ghcr.io/kubenetworks/kubevpn:v1.2.1) newer than server(naison/kubevpn:v1.0.0)",
args: args{
clientVersionStr: "v1.2.1",
clientImgStr: "ghcr.io/kubenetworks/kubevpn:v1.2.1",
serverImgStr: "naison/kubevpn:v1.0.0",
},
want: true,
wantErr: false,
},
// client version: v1.2.1
// client image: ghcr.io/kubenetworks/kubevpn:v1.2.1
// server image: docker.io/naison/kubevpn:v1.0.0
{
name: "Valid case - client(ghcr.io/kubenetworks/kubevpn:v1.2.1) newer than server(docker.io/naison/kubevpn:v1.0.0)",
args: args{
clientVersionStr: "v1.2.1",
clientImgStr: "ghcr.io/kubenetworks/kubevpn:v1.2.1",
serverImgStr: "docker.io/naison/kubevpn:v1.0.0",
},
want: true,
wantErr: false,
},
// client version: v1.2.1
// client image: ghcr.io/kubenetworks/kubevpn:v1.2.1
// server image: naison/kubevpn:v1.2.1
{
name: "Valid case - client(ghcr.io/kubenetworks/kubevpn:v1.2.1) same as server(naison/kubevpn:v1.2.1)",
args: args{
clientVersionStr: "v1.2.1",
clientImgStr: "ghcr.io/kubenetworks/kubevpn:v1.2.1",
serverImgStr: "naison/kubevpn:v1.2.1",
},
want: false,
wantErr: false,
},
// client version: v1.2.1
// client image: ghcr.io/kubenetworks/kubevpn:v1.2.1
// server image: docker.io/naison/kubevpn:v1.2.1
{
name: "Valid case - client(ghcr.io/kubenetworks/kubevpn:v1.2.1) same as server(docker.io/naison/kubevpn:v1.2.1)",
args: args{
clientVersionStr: "v1.2.1",
clientImgStr: "ghcr.io/kubenetworks/kubevpn:v1.2.1",
serverImgStr: "docker.io/naison/kubevpn:v1.2.1",
},
want: false,
wantErr: false,
},
// client version: v1.2.1
// client image: ghcr.io/kubenetworks/kubevpn:v1.2.1
// server image: docker.io/naison/kubevpn:v1.3.1
{
name: "Valid case - client(ghcr.io/kubenetworks/kubevpn:v1.2.1) older as server(docker.io/naison/kubevpn:v1.3.1)",
args: args{
clientVersionStr: "v1.2.1",
clientImgStr: "ghcr.io/kubenetworks/kubevpn:v1.2.1",
serverImgStr: "docker.io/naison/kubevpn:v1.3.1",
},
want: false,
wantErr: false,
},
// client version: v1.3.1
// client image: ghcr.io/kubenetworks/kubevpn:v1.2.1 (not same as client version, --image=xxx)
// server image: ghcr.io/kubenetworks/kubevpn:v1.2.1
{
name: "Valid case - client cli version(v1.3.1) not same as client image(ghcr.io/kubenetworks/kubevpn:v1.2.1)",
args: args{
clientVersionStr: "v1.3.1",
clientImgStr: "ghcr.io/kubenetworks/kubevpn:v1.2.1",
serverImgStr: "ghcr.io/kubenetworks/kubevpn:v1.2.1",
},
want: false,
wantErr: false,
},
// client version: v1.2.1
// client image: ghcr.io/kubenetworks/kubevpn:v1.2.1
// server image: ghcr.io/kubenetworks/kubevpn:v1.0.1
{
name: "Valid case - client(ghcr.io/kubenetworks/kubevpn:v1.2.1) newer than server(ghcr.io/kubenetworks/kubevpn:v1.0.1)",
args: args{
clientVersionStr: "v1.2.1",
clientImgStr: "ghcr.io/kubenetworks/kubevpn:v1.2.1",
serverImgStr: "ghcr.io/kubenetworks/kubevpn:v1.0.1",
},
want: true,
wantErr: false,
},
// client version: v1.2.1
// client image: ghcr.io/kubenetworks/kubevpn:v1.2.1
// server image: ghcr.io/kubenetworks/kubevpn:v1.2.1
{
name: "Valid case - client(ghcr.io/kubenetworks/kubevpn:v1.2.1) same as server(ghcr.io/kubenetworks/kubevpn:v1.2.1)",
args: args{
clientVersionStr: "v1.2.1",
clientImgStr: "ghcr.io/kubenetworks/kubevpn:v1.2.1",
serverImgStr: "ghcr.io/kubenetworks/kubevpn:v1.2.1",
},
want: false,
wantErr: false,
},
// client version: v1.2.1
// client image: ghcr.io/kubenetworks/kubevpn:v1.2.1
// server image: ghcr.io/kubenetworks/kubevpn:v1.3.1
{
name: "Valid case - client(ghcr.io/kubenetworks/kubevpn:v1.2.1) older as server(ghcr.io/kubenetworks/kubevpn:v1.3.1)",
args: args{
clientVersionStr: "v1.2.1",
clientImgStr: "ghcr.io/kubenetworks/kubevpn:v1.2.1",
serverImgStr: "ghcr.io/kubenetworks/kubevpn:v1.3.1",
},
want: false,
wantErr: false,
},
// custom server image registry, but client image is not same as client version, does not upgrade
// client version: v1.2.1
// client image: ghcr.io/kubenetworks/kubevpn:v1.2.1
// server image: mykubevpn.io/kubenetworks/kubevpn:v1.1.1
{
name: "custom server image registry, but client image is not same as client version, does not upgrade",
args: args{
clientVersionStr: "v1.2.1",
clientImgStr: "ghcr.io/kubenetworks/kubevpn:v1.2.1",
serverImgStr: "mykubevpn.io/kubenetworks/kubevpn:v1.1.1",
},
want: false,
wantErr: false,
},
// custom server image registry, client image is same as client version, upgrade
// client version: v1.2.1
// client image: ghcr.io/kubenetworks/kubevpn:v1.2.1
// server image: mykubevpn.io/kubenetworks/kubevpn:v1.1.1
{
name: "custom server image registry, client image is same as client version, upgrade",
args: args{
clientVersionStr: "v1.2.1",
clientImgStr: "mykubevpn.io/kubenetworks/kubevpn:v1.2.1",
serverImgStr: "mykubevpn.io/kubenetworks/kubevpn:v1.1.1",
},
want: true,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := newer(tt.args.clientVersionStr, tt.args.clientImgStr, tt.args.serverImgStr)
if (err != nil) != tt.wantErr {
t.Errorf("newer() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("newer() got = %v, want %v", got, tt.want)
}
})
}
}

View File

@@ -8,7 +8,6 @@ import (
"os/exec"
"reflect"
"runtime"
"strings"
"sync"
"testing"
"time"
@@ -324,11 +323,7 @@ func server(port int) {
func kubevpnConnect(t *testing.T) {
cmd := exec.Command("kubevpn", "proxy", "--debug", "deployments/reviews")
check := func(log string) bool {
line := "+" + strings.Repeat("-", len(log)-2) + "+"
t.Log(line)
t.Log(log)
t.Log(line)
t.Log("\n")
t.Log(util.PrintStr(log))
return false
}
stdout, stderr, err := util.RunWithRollingOutWithChecker(cmd, check)

View File

@@ -1,6 +1,7 @@
package util
import (
"bufio"
"bytes"
"context"
"errors"
@@ -295,21 +296,35 @@ func CleanExtensionLib() {
}
func Print(writer io.Writer, slogan string) {
length := len(slogan) + 1 + 1
str := PrintStr(slogan)
_, _ = writer.Write([]byte(str))
}
func PrintStr(slogan string) string {
scanner := bufio.NewScanner(strings.NewReader(slogan))
var length int
var lines []string
for scanner.Scan() {
line := scanner.Text()
length = max(length, len(line))
lines = append(lines, line)
}
length = length + 1 + 1
var sb strings.Builder
sb.WriteString("+" + strings.Repeat("-", length) + "+")
sb.WriteByte('\n')
sb.WriteString("|")
sb.WriteString(strings.Repeat(" ", 1))
sb.WriteString(slogan)
sb.WriteString(strings.Repeat(" ", 1))
sb.WriteString("|")
for _, line := range lines {
sb.WriteByte('\n')
sb.WriteString("|")
sb.WriteString(strings.Repeat(" ", 1))
sb.WriteString(line)
sb.WriteString(strings.Repeat(" ", length-1-len(line)))
sb.WriteString("|")
}
sb.WriteByte('\n')
sb.WriteString("+" + strings.Repeat("-", length) + "+")
sb.WriteByte('\n')
_, _ = writer.Write([]byte(sb.String()))
return sb.String()
}
func StartupPProf(port int) {

View File

@@ -130,3 +130,34 @@ func TestConvertUidToWorkload(t *testing.T) {
}
}
}
func TestPrintStr(t *testing.T) {
type args struct {
slogan string
}
tests := []struct {
name string
args args
wantWriter string
}{
{
name: "",
args: args{
slogan: "ab\nabc\n\na",
},
wantWriter: `+-----+
| ab |
| abc |
| |
| a |
+-----+`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if gotWriter := PrintStr(tt.args.slogan); gotWriter != tt.wantWriter {
t.Errorf("Print() = %v, want %v", gotWriter, tt.wantWriter)
}
})
}
}

111
pkg/util/version.go Normal file
View File

@@ -0,0 +1,111 @@
package util
import (
"fmt"
"github.com/distribution/reference"
"github.com/hashicorp/go-version"
"github.com/pkg/errors"
)
// CmpClientVersionAndClientImage
/**
version: MAJOR.MINOR.PATCH
client version should match client image
MAJOR and MINOR different should be same, otherwise just exit let use to special matched image with options --image
*/
func CmpClientVersionAndClientImage(clientVersion, clientImgStr string) (bool, error) {
clientImg, err := reference.ParseNormalizedNamed(clientImgStr)
if err != nil {
return false, err
}
clientImgTag, ok := clientImg.(reference.NamedTagged)
if !ok {
return false, fmt.Errorf("can not convert client image")
}
// 1. if client image version is match client cli version, does not need to upgrade
// kubevpn connect --image=ghcr.io/kubenetworks/kubevpn:v2.3.0 or --kubevpnconfig
// the kubevpn version is v2.3.1
if IsVersionMajorOrMinorDiff(clientVersion, clientImgTag.Tag()) {
// exit the process
return true, nil
}
return false, nil
}
// CmpClientVersionAndPodImageTag version MAJOR.MINOR.PATCH
// if MAJOR or MINOR different, needs to upgrade
// otherwise not need upgrade
func CmpClientVersionAndPodImageTag(clientVersion string, serverImgStr string) bool {
serverImg, err := reference.ParseNormalizedNamed(serverImgStr)
if err != nil {
return false
}
serverImgTag, ok := serverImg.(reference.NamedTagged)
if !ok {
return false
}
return IsVersionMajorOrMinorDiff(clientVersion, serverImgTag.Tag())
}
func IsVersionMajorOrMinorDiff(v1 string, v2 string) bool {
version1, err := version.NewVersion(v1)
if err != nil {
return false
}
version2, err := version.NewVersion(v2)
if err != nil {
return false
}
if len(version1.Segments64()) != 3 {
return false
}
if len(version2.Segments64()) != 3 {
return false
}
if version1.Segments64()[0] != version2.Segments64()[0] {
return true
}
if version1.Segments64()[1] != version2.Segments64()[1] {
return true
}
return false
}
func GetTargetImage(version string, image string) string {
serverImg, err := reference.ParseNormalizedNamed(image)
if err != nil {
return ""
}
serverImgTag, ok := serverImg.(reference.NamedTagged)
if !ok {
return ""
}
tag, err := reference.WithTag(serverImgTag, version)
if err != nil {
return ""
}
return tag.String()
}
// IsNewer
/**
version: MAJOR.MINOR.PATCH
MAJOR and MINOR different should be same, otherwise needs upgrade
*/
func IsNewer(clientVer string, clientImg string, serverImg string) (bool, error) {
isNeedUpgrade, _ := CmpClientVersionAndClientImage(clientVer, clientImg)
if isNeedUpgrade {
err := errors.New("\n" + PrintStr(fmt.Sprintf("Current kubevpn cli version is %s, image is: %s, please use the same version of kubevpn image with flag \"--image\"", clientVer, clientImg)))
return true, err
}
if CmpClientVersionAndPodImageTag(clientVer, serverImg) {
return true, nil
}
return false, nil
}

250
pkg/util/version_test.go Normal file
View File

@@ -0,0 +1,250 @@
package util
import "testing"
func Test_newer(t *testing.T) {
type args struct {
clientVersionStr string
clientImgStr string
serverImgStr string
}
tests := []struct {
name string
args args
want bool
wantErr bool
}{
// client version: v1.2.1
// client image: ghcr.io/kubenetworks/kubevpn:v1.2.1
// server image: naison/kubevpn:v1.0.0
{
name: "Valid case - client(ghcr.io/kubenetworks/kubevpn:v1.2.1) newer than server(naison/kubevpn:v1.0.0)",
args: args{
clientVersionStr: "v1.2.1",
clientImgStr: "ghcr.io/kubenetworks/kubevpn:v1.2.1",
serverImgStr: "naison/kubevpn:v1.0.0",
},
want: true,
wantErr: false,
},
// client version: v1.2.1
// client image: ghcr.io/kubenetworks/kubevpn:v1.2.1
// server image: docker.io/naison/kubevpn:v1.0.0
{
name: "Valid case - client(ghcr.io/kubenetworks/kubevpn:v1.2.1) newer than server(docker.io/naison/kubevpn:v1.0.0)",
args: args{
clientVersionStr: "v1.2.1",
clientImgStr: "ghcr.io/kubenetworks/kubevpn:v1.2.1",
serverImgStr: "docker.io/naison/kubevpn:v1.0.0",
},
want: true,
wantErr: false,
},
// client version: v1.2.1
// client image: ghcr.io/kubenetworks/kubevpn:v1.2.1
// server image: naison/kubevpn:v1.2.1
{
name: "Valid case - client(ghcr.io/kubenetworks/kubevpn:v1.2.1) same as server(naison/kubevpn:v1.2.1)",
args: args{
clientVersionStr: "v1.2.1",
clientImgStr: "ghcr.io/kubenetworks/kubevpn:v1.2.1",
serverImgStr: "naison/kubevpn:v1.2.1",
},
want: false,
wantErr: false,
},
// client version: v1.2.1
// client image: ghcr.io/kubenetworks/kubevpn:v1.2.1
// server image: docker.io/naison/kubevpn:v1.2.1
{
name: "Valid case - client(ghcr.io/kubenetworks/kubevpn:v1.2.1) same as server(docker.io/naison/kubevpn:v1.2.1)",
args: args{
clientVersionStr: "v1.2.1",
clientImgStr: "ghcr.io/kubenetworks/kubevpn:v1.2.1",
serverImgStr: "docker.io/naison/kubevpn:v1.2.1",
},
want: false,
wantErr: false,
},
// client version: v1.2.1
// client image: ghcr.io/kubenetworks/kubevpn:v1.2.1
// server image: docker.io/naison/kubevpn:v1.3.1
{
name: "Valid case - client(ghcr.io/kubenetworks/kubevpn:v1.2.1) older as server(docker.io/naison/kubevpn:v1.3.1)",
args: args{
clientVersionStr: "v1.2.1",
clientImgStr: "ghcr.io/kubenetworks/kubevpn:v1.2.1",
serverImgStr: "docker.io/naison/kubevpn:v1.3.1",
},
want: true,
wantErr: false,
},
// client version: v1.3.1
// client image: ghcr.io/kubenetworks/kubevpn:v1.2.1 (not same as client version, --image=xxx)
// server image: ghcr.io/kubenetworks/kubevpn:v1.2.1
{
name: "Valid case - client cli version(v1.3.1) not same as client image(ghcr.io/kubenetworks/kubevpn:v1.2.1)",
args: args{
clientVersionStr: "v1.3.1",
clientImgStr: "ghcr.io/kubenetworks/kubevpn:v1.2.1",
serverImgStr: "ghcr.io/kubenetworks/kubevpn:v1.2.1",
},
want: true,
wantErr: true,
},
// client version: v1.2.1
// client image: ghcr.io/kubenetworks/kubevpn:v1.2.1
// server image: ghcr.io/kubenetworks/kubevpn:v1.0.1
{
name: "Valid case - client(ghcr.io/kubenetworks/kubevpn:v1.2.1) newer than server(ghcr.io/kubenetworks/kubevpn:v1.0.1)",
args: args{
clientVersionStr: "v1.2.1",
clientImgStr: "ghcr.io/kubenetworks/kubevpn:v1.2.1",
serverImgStr: "ghcr.io/kubenetworks/kubevpn:v1.0.1",
},
want: true,
wantErr: false,
},
// client version: v1.2.1
// client image: ghcr.io/kubenetworks/kubevpn:v1.2.1
// server image: ghcr.io/kubenetworks/kubevpn:v1.2.1
{
name: "Valid case - client(ghcr.io/kubenetworks/kubevpn:v1.2.1) same as server(ghcr.io/kubenetworks/kubevpn:v1.2.1)",
args: args{
clientVersionStr: "v1.2.1",
clientImgStr: "ghcr.io/kubenetworks/kubevpn:v1.2.1",
serverImgStr: "ghcr.io/kubenetworks/kubevpn:v1.2.1",
},
want: false,
wantErr: false,
},
// client version: v1.2.1
// client image: ghcr.io/kubenetworks/kubevpn:v1.2.1
// server image: ghcr.io/kubenetworks/kubevpn:v1.3.1
{
name: "Valid case - client(ghcr.io/kubenetworks/kubevpn:v1.2.1) older as server(ghcr.io/kubenetworks/kubevpn:v1.3.1)",
args: args{
clientVersionStr: "v1.2.1",
clientImgStr: "ghcr.io/kubenetworks/kubevpn:v1.2.1",
serverImgStr: "ghcr.io/kubenetworks/kubevpn:v1.3.1",
},
want: true,
wantErr: false,
},
// custom server image registry, but client image is not same as client version, does not upgrade
// client version: v1.2.1
// client image: ghcr.io/kubenetworks/kubevpn:v1.2.1
// server image: mykubevpn.io/kubenetworks/kubevpn:v1.1.1
{
name: "custom server image registry, but client image is not same as client version, does not upgrade",
args: args{
clientVersionStr: "v1.2.1",
clientImgStr: "ghcr.io/kubenetworks/kubevpn:v1.2.1",
serverImgStr: "mykubevpn.io/kubenetworks/kubevpn:v1.1.1",
},
want: true,
wantErr: false,
},
// custom server image registry, client image is same as client version, upgrade
// client version: v1.2.1
// client image: ghcr.io/kubenetworks/kubevpn:v1.2.1
// server image: mykubevpn.io/kubenetworks/kubevpn:v1.1.1
{
name: "custom server image registry, client image is same as client version, upgrade",
args: args{
clientVersionStr: "v1.2.1",
clientImgStr: "mykubevpn.io/kubenetworks/kubevpn:v1.2.1",
serverImgStr: "mykubevpn.io/kubenetworks/kubevpn:v1.1.1",
},
want: true,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := IsNewer(tt.args.clientVersionStr, tt.args.clientImgStr, tt.args.serverImgStr)
if (err != nil) != tt.wantErr {
t.Errorf("newer() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("newer() got = %v, want %v", got, tt.want)
}
})
}
}
func TestGetTargetImage(t *testing.T) {
type args struct {
version string
image string
}
tests := []struct {
name string
args args
want string
}{
{
name: "replace version",
args: args{
version: "v1.2.3",
image: "ghcr.io/kubenetworks/kubevpn:v1.0.0",
},
want: "ghcr.io/kubenetworks/kubevpn:v1.2.3",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := GetTargetImage(tt.args.version, tt.args.image); got != tt.want {
t.Errorf("GetTargetImage() = %v, want %v", got, tt.want)
}
})
}
}
func TestIsVersionMajorOrMinorDiff(t *testing.T) {
type args struct {
clientVersionStr string
serverImgStr string
}
tests := []struct {
name string
args args
want bool
}{
{
name: "Major version is diff",
args: args{
clientVersionStr: "v2.2.3",
serverImgStr: "ghcr.io/kubenetworks/kubevpn:v1.2.0",
},
want: true,
},
{
name: "Minor version is diff",
args: args{
clientVersionStr: "v1.2.3",
serverImgStr: "ghcr.io/kubenetworks/kubevpn:v1.0.0",
},
want: true,
},
{
name: "PATCH version is diff",
args: args{
clientVersionStr: "v2.2.3",
serverImgStr: "ghcr.io/kubenetworks/kubevpn:v2.2.0",
},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := CmpClientVersionAndPodImageTag(tt.args.clientVersionStr, tt.args.serverImgStr); got != tt.want {
t.Errorf("CmpClientVersionAndPodImageTag() = %v, want %v", got, tt.want)
}
})
}
}

214
vendor/k8s.io/kubectl/pkg/scale/scale.go generated vendored Normal file
View File

@@ -0,0 +1,214 @@
/*
Copyright 2014 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package scale
import (
"context"
"fmt"
"strconv"
"time"
autoscalingv1 "k8s.io/api/autoscaling/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/json"
"k8s.io/apimachinery/pkg/util/wait"
scaleclient "k8s.io/client-go/scale"
)
// Scaler provides an interface for resources that can be scaled.
type Scaler interface {
// Scale scales the named resource after checking preconditions. It optionally
// retries in the event of resource version mismatch (if retry is not nil),
// and optionally waits until the status of the resource matches newSize (if wait is not nil)
// TODO: Make the implementation of this watch-based (#56075) once #31345 is fixed.
Scale(namespace, name string, newSize uint, preconditions *ScalePrecondition, retry, wait *RetryParams, gvr schema.GroupVersionResource, dryRun bool) error
// ScaleSimple does a simple one-shot attempt at scaling - not useful on its own, but
// a necessary building block for Scale
ScaleSimple(namespace, name string, preconditions *ScalePrecondition, newSize uint, gvr schema.GroupVersionResource, dryRun bool) (updatedResourceVersion string, err error)
}
// NewScaler get a scaler for a given resource
func NewScaler(scalesGetter scaleclient.ScalesGetter) Scaler {
return &genericScaler{scalesGetter}
}
// ScalePrecondition describes a condition that must be true for the scale to take place
// If CurrentSize == -1, it is ignored.
// If CurrentResourceVersion is the empty string, it is ignored.
// Otherwise they must equal the values in the resource for it to be valid.
type ScalePrecondition struct {
Size int
ResourceVersion string
}
// A PreconditionError is returned when a resource fails to match
// the scale preconditions passed to kubectl.
type PreconditionError struct {
Precondition string
ExpectedValue string
ActualValue string
}
func (pe PreconditionError) Error() string {
return fmt.Sprintf("Expected %s to be %s, was %s", pe.Precondition, pe.ExpectedValue, pe.ActualValue)
}
// RetryParams encapsulates the retry parameters used by kubectl's scaler.
type RetryParams struct {
Interval, Timeout time.Duration
}
func NewRetryParams(interval, timeout time.Duration) *RetryParams {
return &RetryParams{interval, timeout}
}
// ScaleCondition is a closure around Scale that facilitates retries via util.wait
func ScaleCondition(r Scaler, precondition *ScalePrecondition, namespace, name string, count uint, updatedResourceVersion *string, gvr schema.GroupVersionResource, dryRun bool) wait.ConditionFunc {
return func() (bool, error) {
rv, err := r.ScaleSimple(namespace, name, precondition, count, gvr, dryRun)
if updatedResourceVersion != nil {
*updatedResourceVersion = rv
}
// Retry only on update conflicts.
if errors.IsConflict(err) {
return false, nil
}
if err != nil {
return false, err
}
return true, nil
}
}
// validateGeneric ensures that the preconditions match. Returns nil if they are valid, otherwise an error
func (precondition *ScalePrecondition) validate(scale *autoscalingv1.Scale) error {
if precondition.Size != -1 && int(scale.Spec.Replicas) != precondition.Size {
return PreconditionError{"replicas", strconv.Itoa(precondition.Size), strconv.Itoa(int(scale.Spec.Replicas))}
}
if len(precondition.ResourceVersion) > 0 && scale.ResourceVersion != precondition.ResourceVersion {
return PreconditionError{"resource version", precondition.ResourceVersion, scale.ResourceVersion}
}
return nil
}
// genericScaler can update scales for resources in a particular namespace
type genericScaler struct {
scaleNamespacer scaleclient.ScalesGetter
}
var _ Scaler = &genericScaler{}
// ScaleSimple updates a scale of a given resource. It returns the resourceVersion of the scale if the update was successful.
func (s *genericScaler) ScaleSimple(namespace, name string, preconditions *ScalePrecondition, newSize uint, gvr schema.GroupVersionResource, dryRun bool) (updatedResourceVersion string, err error) {
if preconditions != nil {
scale, err := s.scaleNamespacer.Scales(namespace).Get(context.TODO(), gvr.GroupResource(), name, metav1.GetOptions{})
if err != nil {
return "", err
}
if err = preconditions.validate(scale); err != nil {
return "", err
}
scale.Spec.Replicas = int32(newSize)
updateOptions := metav1.UpdateOptions{}
if dryRun {
updateOptions.DryRun = []string{metav1.DryRunAll}
}
updatedScale, err := s.scaleNamespacer.Scales(namespace).Update(context.TODO(), gvr.GroupResource(), scale, updateOptions)
if err != nil {
return "", err
}
return updatedScale.ResourceVersion, nil
}
// objectForReplicas is used for encoding scale patch
type objectForReplicas struct {
Replicas uint `json:"replicas"`
}
// objectForSpec is used for encoding scale patch
type objectForSpec struct {
Spec objectForReplicas `json:"spec"`
}
spec := objectForSpec{
Spec: objectForReplicas{Replicas: newSize},
}
patch, err := json.Marshal(&spec)
if err != nil {
return "", err
}
patchOptions := metav1.PatchOptions{}
if dryRun {
patchOptions.DryRun = []string{metav1.DryRunAll}
}
updatedScale, err := s.scaleNamespacer.Scales(namespace).Patch(context.TODO(), gvr, name, types.MergePatchType, patch, patchOptions)
if err != nil {
return "", err
}
return updatedScale.ResourceVersion, nil
}
// Scale updates a scale of a given resource to a new size, with optional precondition check (if preconditions is not nil),
// optional retries (if retry is not nil), and then optionally waits for the status to reach desired count.
func (s *genericScaler) Scale(namespace, resourceName string, newSize uint, preconditions *ScalePrecondition, retry, waitForReplicas *RetryParams, gvr schema.GroupVersionResource, dryRun bool) error {
if retry == nil {
// make it try only once, immediately
retry = &RetryParams{Interval: time.Millisecond, Timeout: time.Millisecond}
}
cond := ScaleCondition(s, preconditions, namespace, resourceName, newSize, nil, gvr, dryRun)
if err := wait.PollImmediate(retry.Interval, retry.Timeout, cond); err != nil {
return err
}
if waitForReplicas != nil {
return WaitForScaleHasDesiredReplicas(s.scaleNamespacer, gvr.GroupResource(), resourceName, namespace, newSize, waitForReplicas)
}
return nil
}
// scaleHasDesiredReplicas returns a condition that will be true if and only if the desired replica
// count for a scale (Spec) equals its updated replicas count (Status)
func scaleHasDesiredReplicas(sClient scaleclient.ScalesGetter, gr schema.GroupResource, resourceName string, namespace string, desiredReplicas int32) wait.ConditionFunc {
return func() (bool, error) {
actualScale, err := sClient.Scales(namespace).Get(context.TODO(), gr, resourceName, metav1.GetOptions{})
if err != nil {
return false, err
}
// this means the desired scale target has been reset by something else
if actualScale.Spec.Replicas != desiredReplicas {
return true, nil
}
return actualScale.Spec.Replicas == actualScale.Status.Replicas &&
desiredReplicas == actualScale.Status.Replicas, nil
}
}
// WaitForScaleHasDesiredReplicas waits until condition scaleHasDesiredReplicas is satisfied
// or returns error when timeout happens
func WaitForScaleHasDesiredReplicas(sClient scaleclient.ScalesGetter, gr schema.GroupResource, resourceName string, namespace string, newSize uint, waitForReplicas *RetryParams) error {
if waitForReplicas == nil {
return fmt.Errorf("waitForReplicas parameter cannot be nil")
}
err := wait.PollImmediate(
waitForReplicas.Interval,
waitForReplicas.Timeout,
scaleHasDesiredReplicas(sClient, gr, resourceName, namespace, int32(newSize)))
if err == wait.ErrWaitTimeout {
return fmt.Errorf("timed out waiting for %q to be synced", resourceName)
}
return err
}

1
vendor/modules.txt vendored
View File

@@ -2358,6 +2358,7 @@ k8s.io/kubectl/pkg/generate
k8s.io/kubectl/pkg/generate/versioned
k8s.io/kubectl/pkg/polymorphichelpers
k8s.io/kubectl/pkg/rawhttp
k8s.io/kubectl/pkg/scale
k8s.io/kubectl/pkg/scheme
k8s.io/kubectl/pkg/util/certificate
k8s.io/kubectl/pkg/util/completion