github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/cli/cmd/cluster/describe_ops_test.go (about)

     1  /*
     2  Copyright (C) 2022-2023 ApeCloud Co., Ltd
     3  
     4  This file is part of KubeBlocks project
     5  
     6  This program is free software: you can redistribute it and/or modify
     7  it under the terms of the GNU Affero General Public License as published by
     8  the Free Software Foundation, either version 3 of the License, or
     9  (at your option) any later version.
    10  
    11  This program is distributed in the hope that it will be useful
    12  but WITHOUT ANY WARRANTY; without even the implied warranty of
    13  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    14  GNU Affero General Public License for more details.
    15  
    16  You should have received a copy of the GNU Affero General Public License
    17  along with this program.  If not, see <http://www.gnu.org/licenses/>.
    18  */
    19  
    20  package cluster
    21  
    22  import (
    23  	"bytes"
    24  	"fmt"
    25  	"net/http"
    26  	"time"
    27  
    28  	. "github.com/onsi/ginkgo/v2"
    29  	. "github.com/onsi/gomega"
    30  	corev1 "k8s.io/api/core/v1"
    31  	apiresource "k8s.io/apimachinery/pkg/api/resource"
    32  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    33  	"k8s.io/apimachinery/pkg/runtime"
    34  	"k8s.io/apimachinery/pkg/runtime/schema"
    35  	"k8s.io/cli-runtime/pkg/genericiooptions"
    36  	"k8s.io/cli-runtime/pkg/resource"
    37  	"k8s.io/client-go/kubernetes/scheme"
    38  	clientfake "k8s.io/client-go/rest/fake"
    39  	cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
    40  
    41  	appsv1alpha1 "github.com/1aal/kubeblocks/apis/apps/v1alpha1"
    42  	clitesting "github.com/1aal/kubeblocks/pkg/cli/testing"
    43  	"github.com/1aal/kubeblocks/pkg/cli/types"
    44  )
    45  
    46  var _ = Describe("Expose", func() {
    47  	const (
    48  		namespace          = "test"
    49  		opsName            = "test-ops"
    50  		componentName      = "test_stateful"
    51  		componentName1     = "test_stateless"
    52  		clusterVersionName = "test-cluster-0.1"
    53  	)
    54  
    55  	var (
    56  		streams genericiooptions.IOStreams
    57  		tf      *cmdtesting.TestFactory
    58  	)
    59  
    60  	BeforeEach(func() {
    61  		streams, _, _, _ = genericiooptions.NewTestIOStreams()
    62  		tf = clitesting.NewTestFactory(namespace)
    63  		codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
    64  		httpResp := func(obj runtime.Object) *http.Response {
    65  			return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, obj)}
    66  		}
    67  
    68  		tf.UnstructuredClient = &clientfake.RESTClient{
    69  			GroupVersion:         schema.GroupVersion{Group: types.AppsAPIGroup, Version: types.AppsAPIVersion},
    70  			NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
    71  			Client: clientfake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
    72  				urlPrefix := "/api/v1/namespaces/" + namespace
    73  				mapping := map[string]*http.Response{
    74  					"/api/v1/nodes/" + clitesting.NodeName: httpResp(clitesting.FakeNode()),
    75  					urlPrefix + "/services":                httpResp(&corev1.ServiceList{}),
    76  					urlPrefix + "/events":                  httpResp(&corev1.EventList{}),
    77  					// urlPrefix + "/pods":                 httpResp(pods),
    78  				}
    79  				return mapping[req.URL.Path], nil
    80  			}),
    81  		}
    82  
    83  		tf.Client = tf.UnstructuredClient
    84  	})
    85  
    86  	AfterEach(func() {
    87  		tf.Cleanup()
    88  	})
    89  
    90  	It("describe", func() {
    91  		cmd := NewDescribeOpsCmd(tf, streams)
    92  		Expect(cmd).ShouldNot(BeNil())
    93  	})
    94  
    95  	It("complete", func() {
    96  		o := newDescribeOpsOptions(tf, streams)
    97  		Expect(o.complete(nil).Error()).Should(Equal("OpsRequest name should be specified"))
    98  		Expect(o.complete([]string{opsName})).Should(Succeed())
    99  		Expect(o.names).Should(Equal([]string{opsName}))
   100  		Expect(o.client).ShouldNot(BeNil())
   101  		Expect(o.dynamic).ShouldNot(BeNil())
   102  		Expect(o.namespace).Should(Equal(namespace))
   103  	})
   104  
   105  	generateOpsObject := func(opsName string, opsType appsv1alpha1.OpsType) *appsv1alpha1.OpsRequest {
   106  		return &appsv1alpha1.OpsRequest{
   107  			ObjectMeta: metav1.ObjectMeta{
   108  				Name:      opsName,
   109  				Namespace: namespace,
   110  			},
   111  			Spec: appsv1alpha1.OpsRequestSpec{
   112  				ClusterRef: "test-cluster",
   113  				Type:       opsType,
   114  			},
   115  		}
   116  	}
   117  
   118  	describeOps := func(opsType appsv1alpha1.OpsType, completeOps func(ops *appsv1alpha1.OpsRequest)) {
   119  		randomStr := clitesting.GetRandomStr()
   120  		ops := generateOpsObject(opsName+randomStr, opsType)
   121  		completeOps(ops)
   122  		tf.FakeDynamicClient = clitesting.FakeDynamicClient(ops)
   123  		o := newDescribeOpsOptions(tf, streams)
   124  		Expect(o.complete([]string{opsName + randomStr})).Should(Succeed())
   125  		Expect(o.run()).Should(Succeed())
   126  	}
   127  
   128  	fakeOpsStatusAndProgress := func() appsv1alpha1.OpsRequestStatus {
   129  		objectKey := "Pod/test-pod-wessxd"
   130  		objectKey1 := "Pod/test-pod-xsdfwe"
   131  		return appsv1alpha1.OpsRequestStatus{
   132  			StartTimestamp:      metav1.NewTime(time.Now().Add(-1 * time.Minute)),
   133  			CompletionTimestamp: metav1.NewTime(time.Now()),
   134  			Progress:            "1/2",
   135  			Phase:               appsv1alpha1.OpsFailedPhase,
   136  			Components: map[string]appsv1alpha1.OpsRequestComponentStatus{
   137  				componentName: {
   138  					Phase: appsv1alpha1.FailedClusterCompPhase,
   139  					ProgressDetails: []appsv1alpha1.ProgressStatusDetail{
   140  						{
   141  							ObjectKey: objectKey,
   142  							Status:    appsv1alpha1.SucceedProgressStatus,
   143  							StartTime: metav1.NewTime(time.Now().Add(-59 * time.Second)),
   144  							EndTime:   metav1.NewTime(time.Now().Add(-39 * time.Second)),
   145  							Message:   fmt.Sprintf("Successfully vertical scale Pod: %s in Component: %s", objectKey, componentName),
   146  						},
   147  						{
   148  							ObjectKey: objectKey1,
   149  							Status:    appsv1alpha1.FailedProgressStatus,
   150  							StartTime: metav1.NewTime(time.Now().Add(-39 * time.Second)),
   151  							EndTime:   metav1.NewTime(time.Now().Add(-1 * time.Second)),
   152  							Message:   fmt.Sprintf("Failed to vertical scale Pod: %s in Component: %s", objectKey1, componentName),
   153  						},
   154  					},
   155  				},
   156  			},
   157  			Conditions: []metav1.Condition{
   158  				{
   159  					Type:    "Processing",
   160  					Reason:  "ProcessingOps",
   161  					Status:  metav1.ConditionTrue,
   162  					Message: "Start to process the OpsRequest.",
   163  				},
   164  				{
   165  					Type:    "Failed",
   166  					Reason:  "FailedScale",
   167  					Status:  metav1.ConditionFalse,
   168  					Message: "Failed to process the OpsRequest.",
   169  				},
   170  			},
   171  		}
   172  	}
   173  
   174  	testPrintLastConfiguration := func(config appsv1alpha1.LastConfiguration,
   175  		opsType appsv1alpha1.OpsType, expectStrings ...string) {
   176  		o := newDescribeOpsOptions(tf, streams)
   177  		if opsType == appsv1alpha1.UpgradeType {
   178  			// capture stdout
   179  			done := clitesting.Capture()
   180  			o.printLastConfiguration(config, opsType)
   181  			capturedOutput, err := done()
   182  			Expect(err).Should(Succeed())
   183  			Expect(clitesting.ContainExpectStrings(capturedOutput, expectStrings...)).Should(BeTrue())
   184  			return
   185  		}
   186  		o.printLastConfiguration(config, opsType)
   187  		out := o.Out.(*bytes.Buffer)
   188  		Expect(clitesting.ContainExpectStrings(out.String(), expectStrings...)).Should(BeTrue())
   189  	}
   190  
   191  	It("run", func() {
   192  		By("test describe Upgrade")
   193  		describeOps(appsv1alpha1.UpgradeType, func(ops *appsv1alpha1.OpsRequest) {
   194  			ops.Spec.Upgrade = &appsv1alpha1.Upgrade{
   195  				ClusterVersionRef: clusterVersionName,
   196  			}
   197  		})
   198  
   199  		By("test describe Restart")
   200  		describeOps(appsv1alpha1.RestartType, func(ops *appsv1alpha1.OpsRequest) {
   201  			ops.Spec.RestartList = []appsv1alpha1.ComponentOps{
   202  				{ComponentName: componentName},
   203  				{ComponentName: componentName1},
   204  			}
   205  		})
   206  
   207  		By("test describe VerticalScaling")
   208  		resourceRequirements := corev1.ResourceRequirements{
   209  			Requests: corev1.ResourceList{
   210  				"cpu":    apiresource.MustParse("100m"),
   211  				"memory": apiresource.MustParse("200Mi"),
   212  			},
   213  			Limits: corev1.ResourceList{
   214  				"cpu":    apiresource.MustParse("300m"),
   215  				"memory": apiresource.MustParse("400Mi"),
   216  			},
   217  		}
   218  		fakeVerticalScalingSpec := func() []appsv1alpha1.VerticalScaling {
   219  			return []appsv1alpha1.VerticalScaling{
   220  				{
   221  					ComponentOps: appsv1alpha1.ComponentOps{
   222  						ComponentName: componentName,
   223  					},
   224  					ResourceRequirements: resourceRequirements,
   225  				},
   226  			}
   227  		}
   228  		describeOps(appsv1alpha1.VerticalScalingType, func(ops *appsv1alpha1.OpsRequest) {
   229  			ops.Spec.VerticalScalingList = fakeVerticalScalingSpec()
   230  		})
   231  
   232  		By("test describe HorizontalScaling")
   233  		describeOps(appsv1alpha1.HorizontalScalingType, func(ops *appsv1alpha1.OpsRequest) {
   234  			ops.Spec.HorizontalScalingList = []appsv1alpha1.HorizontalScaling{
   235  				{
   236  					ComponentOps: appsv1alpha1.ComponentOps{
   237  						ComponentName: componentName,
   238  					},
   239  					Replicas: 1,
   240  				},
   241  			}
   242  		})
   243  
   244  		By("test describe VolumeExpansion and print OpsRequest status")
   245  		volumeClaimTemplates := []appsv1alpha1.OpsRequestVolumeClaimTemplate{
   246  			{
   247  				Name:    "data",
   248  				Storage: apiresource.MustParse("2Gi"),
   249  			},
   250  			{
   251  				Name:    "log",
   252  				Storage: apiresource.MustParse("4Gi"),
   253  			},
   254  		}
   255  		describeOps(appsv1alpha1.VolumeExpansionType, func(ops *appsv1alpha1.OpsRequest) {
   256  			ops.Spec.VolumeExpansionList = []appsv1alpha1.VolumeExpansion{
   257  				{
   258  					ComponentOps: appsv1alpha1.ComponentOps{
   259  						ComponentName: componentName,
   260  					},
   261  					VolumeClaimTemplates: volumeClaimTemplates,
   262  				},
   263  			}
   264  		})
   265  
   266  		By("test printing OpsRequest status and conditions")
   267  		describeOps(appsv1alpha1.VerticalScalingType, func(ops *appsv1alpha1.OpsRequest) {
   268  			ops.Spec.VerticalScalingList = fakeVerticalScalingSpec()
   269  			ops.Status = fakeOpsStatusAndProgress()
   270  		})
   271  
   272  		By("test printing OpsRequest last configuration")
   273  		testPrintLastConfiguration(appsv1alpha1.LastConfiguration{
   274  			ClusterVersionRef: clusterVersionName,
   275  		}, appsv1alpha1.UpgradeType, "\nLast Configuration",
   276  			fmt.Sprintf("%-20s%s", "Cluster Version:", clusterVersionName+"\n"))
   277  
   278  		By("test verticalScaling last configuration")
   279  		testPrintLastConfiguration(appsv1alpha1.LastConfiguration{
   280  			Components: map[string]appsv1alpha1.LastComponentConfiguration{
   281  				componentName: {
   282  					ResourceRequirements: resourceRequirements,
   283  				},
   284  			},
   285  		}, appsv1alpha1.VerticalScalingType, "100m", "200Mi", "300m", "400Mi",
   286  			"REQUEST-CPU", "REQUEST-MEMORY", "LIMIT-CPU", "LIMIT-MEMORY")
   287  
   288  		By("test HorizontalScaling last configuration")
   289  		replicas := int32(2)
   290  		testPrintLastConfiguration(appsv1alpha1.LastConfiguration{
   291  			Components: map[string]appsv1alpha1.LastComponentConfiguration{
   292  				componentName: {
   293  					Replicas: &replicas,
   294  				},
   295  			},
   296  		}, appsv1alpha1.HorizontalScalingType, "COMPONENT", "REPLICAS", componentName, "2")
   297  
   298  		By("test VolumeExpansion last configuration")
   299  		testPrintLastConfiguration(appsv1alpha1.LastConfiguration{
   300  			Components: map[string]appsv1alpha1.LastComponentConfiguration{
   301  				componentName: {
   302  					VolumeClaimTemplates: volumeClaimTemplates,
   303  				},
   304  			},
   305  		}, appsv1alpha1.VolumeExpansionType, "VOLUME-CLAIM-TEMPLATE", "STORAGE", "data", "2Gi", "log")
   306  
   307  	})
   308  })