github.com/oam-dev/kubevela@v1.9.11/pkg/controller/core.oam.dev/v1beta1/application/apply_test.go (about)

     1  /*
     2  Copyright 2021 The KubeVela Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package application
    18  
    19  import (
    20  	"context"
    21  	"strconv"
    22  	"strings"
    23  	"testing"
    24  	"time"
    25  
    26  	"github.com/oam-dev/kubevela/pkg/oam/testutil"
    27  
    28  	terraformtypes "github.com/oam-dev/terraform-controller/api/types"
    29  	terraformapi "github.com/oam-dev/terraform-controller/api/v1beta2"
    30  	. "github.com/onsi/ginkgo/v2"
    31  	. "github.com/onsi/gomega"
    32  	appsv1 "k8s.io/api/apps/v1"
    33  	corev1 "k8s.io/api/core/v1"
    34  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    35  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    36  	"k8s.io/apimachinery/pkg/runtime"
    37  	"k8s.io/apimachinery/pkg/types"
    38  	"k8s.io/utils/pointer"
    39  	"sigs.k8s.io/controller-runtime/pkg/reconcile"
    40  	"sigs.k8s.io/yaml"
    41  
    42  	"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
    43  	"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
    44  	"github.com/oam-dev/kubevela/pkg/oam/util"
    45  )
    46  
    47  const workloadDefinition = `
    48  apiVersion: core.oam.dev/v1beta1
    49  kind: WorkloadDefinition
    50  metadata:
    51    name: test-worker
    52    annotations:
    53      definition.oam.dev/description: "Describes long-running, scalable, containerized services that running at backend. They do NOT have network endpoint to receive external network traffic."
    54  spec:
    55    workload:
    56      definition:
    57        apiVersion: apps/v1
    58        kind: Deployment
    59    schematic:
    60      cue:
    61        template: |
    62          output: {
    63            apiVersion: "apps/v1"
    64            kind:       "Deployment"
    65            spec: {
    66              selector: matchLabels: {
    67                "app.oam.dev/component": context.name
    68              }
    69              template: {
    70                metadata: labels: {
    71                  "app.oam.dev/component": context.name
    72                }
    73                spec: {
    74                  containers: [{
    75                    name:  context.name
    76                    image: parameter.image
    77  
    78                    if parameter["cmd"] != _|_ {
    79                      command: parameter.cmd
    80                    }
    81                  }]
    82                }
    83              }
    84            }
    85          }
    86          parameter: {
    87            image: string
    88            cmd?: [...string]
    89          }
    90  `
    91  
    92  var _ = Describe("Test Application apply", func() {
    93  	var app *v1beta1.Application
    94  	var namespaceName string
    95  	var ns corev1.Namespace
    96  
    97  	BeforeEach(func() {
    98  		ctx := context.TODO()
    99  		namespaceName = "apply-test-" + strconv.Itoa(time.Now().Second()) + "-" + strconv.Itoa(time.Now().Nanosecond())
   100  		ns = corev1.Namespace{
   101  			ObjectMeta: metav1.ObjectMeta{
   102  				Name: namespaceName,
   103  			},
   104  		}
   105  		app = &v1beta1.Application{
   106  			TypeMeta: metav1.TypeMeta{
   107  				Kind:       "Application",
   108  				APIVersion: "core.oam.dev/v1beta1",
   109  			},
   110  		}
   111  		app.Namespace = namespaceName
   112  		app.Spec = v1beta1.ApplicationSpec{
   113  			Components: []common.ApplicationComponent{{
   114  				Type: "test-worker",
   115  				Name: "test-app",
   116  				Properties: &runtime.RawExtension{
   117  					Raw: []byte(`{"image": "oamdev/testapp:v1", "cmd": ["node", "server.js"]}`),
   118  				},
   119  			}},
   120  		}
   121  		By("Create the Namespace for test")
   122  		Expect(k8sClient.Create(ctx, &ns)).Should(Succeed())
   123  	})
   124  
   125  	AfterEach(func() {
   126  		By("[TEST] Clean up resources after an integration test")
   127  		Expect(k8sClient.Delete(context.TODO(), &ns)).Should(Succeed())
   128  	})
   129  
   130  	It("Test update or create app revision", func() {
   131  		ctx := context.TODO()
   132  		By("[TEST] Create a workload definition")
   133  		var deployDef v1beta1.WorkloadDefinition
   134  		Expect(yaml.Unmarshal([]byte(workloadDefinition), &deployDef)).Should(BeNil())
   135  		deployDef.Namespace = app.Namespace
   136  		Expect(k8sClient.Create(ctx, &deployDef)).Should(SatisfyAny(BeNil()))
   137  
   138  		By("[TEST] Create a application")
   139  		app.Name = "poc"
   140  		err := k8sClient.Create(ctx, app)
   141  		Expect(err).Should(BeNil())
   142  
   143  		By("[TEST] get a application")
   144  		testutil.ReconcileOnceAfterFinalizer(reconciler, reconcile.Request{NamespacedName: types.NamespacedName{Name: app.Name, Namespace: app.Namespace}})
   145  		testapp := v1beta1.Application{}
   146  		err = k8sClient.Get(ctx, types.NamespacedName{Name: app.Name, Namespace: app.Namespace}, &testapp)
   147  		Expect(err).Should(BeNil())
   148  		Expect(testapp.Status.LatestRevision != nil).Should(BeTrue())
   149  
   150  		By("[TEST] get a application revision")
   151  		appRevName := testapp.Status.LatestRevision.Name
   152  		apprev := &v1beta1.ApplicationRevision{}
   153  		err = k8sClient.Get(ctx, types.NamespacedName{Name: appRevName, Namespace: app.Namespace}, apprev)
   154  		Expect(err).Should(BeNil())
   155  
   156  		By("[TEST] verify that the revision is exist and set correctly")
   157  		applabel, exist := apprev.Labels["app.oam.dev/name"]
   158  		Expect(exist).Should(BeTrue())
   159  		Expect(strings.Compare(applabel, app.Name) == 0).Should(BeTrue())
   160  	})
   161  })
   162  
   163  var _ = Describe("Test deleter resource", func() {
   164  	It("Test delete resource will remove ref from reference", func() {
   165  		deployName := "test-del-resource-workload"
   166  		namespace := "test-del-resource-namespace"
   167  		ctx := context.Background()
   168  		Expect(k8sClient.Create(ctx, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: namespace}})).Should(BeNil())
   169  		deploy := appsv1.Deployment{
   170  			TypeMeta: metav1.TypeMeta{
   171  				APIVersion: "apps/v1",
   172  				Kind:       "Deployment",
   173  			},
   174  			ObjectMeta: metav1.ObjectMeta{
   175  				Namespace: namespace,
   176  				Name:      deployName,
   177  			},
   178  			Spec: appsv1.DeploymentSpec{
   179  				Replicas: pointer.Int32(3),
   180  				Selector: &metav1.LabelSelector{
   181  					MatchLabels: map[string]string{
   182  						"app": "test",
   183  					},
   184  				},
   185  				Template: corev1.PodTemplateSpec{
   186  					ObjectMeta: metav1.ObjectMeta{
   187  						Labels: map[string]string{
   188  							"app": "test",
   189  						},
   190  					},
   191  					Spec: corev1.PodSpec{
   192  						Containers: []corev1.Container{
   193  							{
   194  								Name:  "test-container",
   195  								Image: "test-image",
   196  							},
   197  						},
   198  					},
   199  				},
   200  			},
   201  		}
   202  		Expect(k8sClient.Create(ctx, &deploy)).Should(BeNil())
   203  		u := unstructured.Unstructured{}
   204  		u.SetAPIVersion("apps/v1")
   205  		u.SetKind("Deployment")
   206  		Expect(k8sClient.Get(ctx, types.NamespacedName{Name: deployName, Namespace: namespace}, &u)).Should(BeNil())
   207  		appliedRsc := []common.ClusterObjectReference{
   208  			{
   209  				Creator: common.WorkflowResourceCreator,
   210  				ObjectReference: corev1.ObjectReference{
   211  					Kind:       u.GetKind(),
   212  					APIVersion: u.GetAPIVersion(),
   213  					Namespace:  u.GetNamespace(),
   214  					Name:       deployName,
   215  				},
   216  			},
   217  			{
   218  				Creator: common.WorkflowResourceCreator,
   219  				ObjectReference: corev1.ObjectReference{
   220  					Kind:       "StatefulSet",
   221  					APIVersion: "apps/v1",
   222  					Namespace:  "test-namespace",
   223  					Name:       "test-sts",
   224  				},
   225  			},
   226  		}
   227  		h, err := NewAppHandler(ctx, reconciler, &v1beta1.Application{ObjectMeta: metav1.ObjectMeta{Name: "example", Namespace: "default"}})
   228  		Expect(err).Should(Succeed())
   229  		h.appliedResources = appliedRsc
   230  		Expect(h.Delete(ctx, "", common.WorkflowResourceCreator, &u))
   231  		checkDeploy := unstructured.Unstructured{}
   232  		checkDeploy.SetAPIVersion("apps/v1")
   233  		checkDeploy.SetKind("Deployment")
   234  		Expect(k8sClient.Get(ctx, types.NamespacedName{Name: deployName, Namespace: namespace}, &u)).Should(SatisfyAny(util.NotFoundMatcher{}))
   235  		Expect(len(h.appliedResources)).Should(BeEquivalentTo(1))
   236  		Expect(h.appliedResources[0].Kind).Should(BeEquivalentTo("StatefulSet"))
   237  		Expect(h.appliedResources[0].Name).Should(BeEquivalentTo("test-sts"))
   238  	})
   239  })
   240  
   241  func TestDeleteAppliedResourceFunc(t *testing.T) {
   242  	h := AppHandler{appliedResources: []common.ClusterObjectReference{
   243  		{
   244  			ObjectReference: corev1.ObjectReference{
   245  				Name: "wl-1",
   246  				Kind: "Deployment",
   247  			},
   248  		},
   249  		{
   250  			ObjectReference: corev1.ObjectReference{
   251  				Name: "wl-2",
   252  				Kind: "Deployment",
   253  			},
   254  		},
   255  		{
   256  			ObjectReference: corev1.ObjectReference{
   257  				Name: "wl-1",
   258  				Kind: "StatefulSet",
   259  			},
   260  		},
   261  		{
   262  			Cluster: "runtime-cluster",
   263  			ObjectReference: corev1.ObjectReference{
   264  				Name: "wl-1",
   265  				Kind: "StatefulSet",
   266  			},
   267  		},
   268  	}}
   269  	deleteResc_1 := common.ClusterObjectReference{ObjectReference: corev1.ObjectReference{Name: "wl-1", Kind: "StatefulSet"}, Cluster: "runtime-cluster"}
   270  	deleteResc_2 := common.ClusterObjectReference{ObjectReference: corev1.ObjectReference{Name: "wl-2", Kind: "Deployment"}}
   271  	h.deleteAppliedResource(deleteResc_1)
   272  	h.deleteAppliedResource(deleteResc_2)
   273  	if len(h.appliedResources) != 2 {
   274  		t.Errorf("applied length error acctually %d", len(h.appliedResources))
   275  	}
   276  	if h.appliedResources[0].Name != "wl-1" || h.appliedResources[0].Kind != "Deployment" {
   277  		t.Errorf("resource missmatch")
   278  	}
   279  	if h.appliedResources[1].Name != "wl-1" || h.appliedResources[1].Kind != "StatefulSet" {
   280  		t.Errorf("resource missmatch")
   281  	}
   282  
   283  	preDelResc := common.ClusterObjectReference{ObjectReference: corev1.ObjectReference{Name: "wl-3", Kind: "StatefulSet"}, Cluster: "runtime-cluster"}
   284  	h.deleteAppliedResource(preDelResc)
   285  	h.addAppliedResource(true, preDelResc)
   286  	if len(h.appliedResources) != 2 {
   287  		t.Errorf("applied length error acctually %d", len(h.appliedResources))
   288  	}
   289  }
   290  
   291  var _ = Describe("Test Application health check", func() {
   292  	const (
   293  		timeout  = time.Second * 10
   294  		duration = time.Second * 10
   295  		interval = time.Millisecond * 500
   296  	)
   297  
   298  	var (
   299  		ctx context.Context
   300  		ns  corev1.Namespace
   301  	)
   302  
   303  	BeforeEach(func() {
   304  		ctx = context.TODO()
   305  		namespaceName := "health-check-test-" + strconv.Itoa(time.Now().Second()) + "-" + strconv.Itoa(time.Now().Nanosecond())
   306  		ns = corev1.Namespace{
   307  			ObjectMeta: metav1.ObjectMeta{
   308  				Name: namespaceName,
   309  			},
   310  		}
   311  		By("By create the Namespace for test")
   312  		Expect(k8sClient.Create(ctx, &ns)).Should(Succeed())
   313  	})
   314  
   315  	AfterEach(func() {
   316  		By("By clean up resources after an integration test")
   317  		Expect(k8sClient.Delete(ctx, &ns)).Should(Succeed())
   318  	})
   319  
   320  	Context("Terraform components", func() {
   321  		var (
   322  			componentNamespace = "vela-system"
   323  			componentName      string
   324  			comp               v1beta1.ComponentDefinition
   325  
   326  			applicationName = "test-tf-health-app"
   327  		)
   328  
   329  		BeforeEach(func() {
   330  			componentName = "demo-hello-tf" + strconv.Itoa(time.Now().Second()) + "-" + strconv.Itoa(time.Now().Nanosecond())
   331  			comp = v1beta1.ComponentDefinition{
   332  				TypeMeta: metav1.TypeMeta{
   333  					APIVersion: v1beta1.ComponentDefinitionKindAPIVersion,
   334  					Kind:       v1beta1.ComponentDefinitionKind,
   335  				},
   336  				ObjectMeta: metav1.ObjectMeta{
   337  					Name:      componentName,
   338  					Namespace: componentNamespace,
   339  					Labels: map[string]string{
   340  						"type": "terraform",
   341  					},
   342  				},
   343  				Spec: v1beta1.ComponentDefinitionSpec{
   344  					Workload: common.WorkloadTypeDescriptor{
   345  						Definition: common.WorkloadGVK{
   346  							APIVersion: "terraform.core.oam.dev/v1beta2",
   347  							Kind:       "Configuration",
   348  						},
   349  					},
   350  					Schematic: &common.Schematic{
   351  						Terraform: &common.Terraform{
   352  							Configuration: `
   353  							terraform {}
   354  
   355  							variable "name" {
   356  							  type        = string
   357  							  description = "your name"
   358  							}
   359  							
   360  							output "message" {
   361  							  value = "hello, ${var.name}"
   362  							}`,
   363  						},
   364  					},
   365  				},
   366  			}
   367  			By("By create terraform component")
   368  			Expect(k8sClient.Create(ctx, &comp)).Should(Succeed())
   369  		})
   370  
   371  		AfterEach(func() {
   372  			By("By clean terraform component")
   373  			Expect(k8sClient.Delete(ctx, &comp)).Should(Succeed())
   374  		})
   375  
   376  		It("Should terraform components stand ready and the latest when a new application is running", func() {
   377  			By("By creating a new app")
   378  			tfCompName := applicationName + "-comp"
   379  			app := v1beta1.Application{
   380  				TypeMeta: metav1.TypeMeta{
   381  					APIVersion: v1beta1.ApplicationKindAPIVersion,
   382  					Kind:       v1beta1.ApplicationKind,
   383  				},
   384  				ObjectMeta: metav1.ObjectMeta{
   385  					Name:      applicationName,
   386  					Namespace: ns.Name,
   387  				},
   388  				Spec: v1beta1.ApplicationSpec{
   389  					Components: []common.ApplicationComponent{
   390  						{
   391  							Name: tfCompName,
   392  							Type: componentName,
   393  							Properties: &runtime.RawExtension{
   394  								Raw: []byte(`{"name": "vela!"}`),
   395  							},
   396  						},
   397  					},
   398  				},
   399  			}
   400  			Expect(k8sClient.Create(ctx, &app)).Should(Succeed())
   401  
   402  			applicationLoopupKey := types.NamespacedName{Name: applicationName, Namespace: ns.Name}
   403  			testutil.ReconcileOnceAfterFinalizer(reconciler, reconcile.Request{NamespacedName: applicationLoopupKey})
   404  
   405  			configurationLookupKey := types.NamespacedName{Name: tfCompName, Namespace: ns.Name}
   406  			configuration := &terraformapi.Configuration{}
   407  
   408  			Eventually(func() error {
   409  				return k8sClient.Get(ctx, configurationLookupKey, configuration)
   410  			}, timeout, interval).Should(Succeed())
   411  
   412  			By("By checking the Application status is not running")
   413  			Consistently(func() (common.ApplicationPhase, error) {
   414  				err := k8sClient.Get(ctx, applicationLoopupKey, &app)
   415  				if err != nil {
   416  					return "", err
   417  				}
   418  				return app.Status.Phase, nil
   419  			}, duration, interval).ShouldNot(Equal(common.ApplicationRunning))
   420  
   421  			By("By configuration is ready")
   422  			configuration.Status = terraformapi.ConfigurationStatus{
   423  				ObservedGeneration: configuration.Generation,
   424  				Apply: terraformapi.ConfigurationApplyStatus{
   425  					State: terraformtypes.Available,
   426  				},
   427  			}
   428  			Expect(k8sClient.Status().Update(ctx, configuration)).Should(Succeed())
   429  
   430  			By("By checking that the Application status is running")
   431  			Eventually(func() common.ApplicationPhase {
   432  				testutil.ReconcileOnce(reconciler, reconcile.Request{NamespacedName: applicationLoopupKey})
   433  				err := k8sClient.Get(ctx, applicationLoopupKey, &app)
   434  				if err != nil {
   435  					return ""
   436  				}
   437  				return app.Status.Phase
   438  			}, timeout, interval).Should(Equal(common.ApplicationRunning))
   439  
   440  			By("By update the Application and check that the status is not running")
   441  			app.Spec.Components[0].Properties.Raw = []byte(`{"name": "terraform!"}`)
   442  			Expect(k8sClient.Update(ctx, &app)).Should(Succeed())
   443  
   444  			Consistently(func() (common.ApplicationPhase, error) {
   445  				testutil.ReconcileOnce(reconciler, reconcile.Request{NamespacedName: applicationLoopupKey})
   446  				if err := k8sClient.Get(ctx, applicationLoopupKey, &app); err != nil {
   447  					return "", err
   448  				}
   449  				return app.Status.Phase, nil
   450  			}, duration, interval).ShouldNot(Equal(common.ApplicationRunning))
   451  		})
   452  	})
   453  })