mirror of
https://github.com/kubevela/kubevela.git
synced 2026-08-27 16:17:34 +00:00
realted to refactor application (#1712)
implement app/assemble pkg add unit test add log Signed-off-by: roy wang <seiwy2010@gmail.com>
This commit is contained in:
@@ -0,0 +1,400 @@
|
||||
/*
|
||||
Copyright 2021 The KubeVela 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 assemble
|
||||
|
||||
import (
|
||||
runtimev1alpha1 "github.com/crossplane/crossplane-runtime/apis/core/v1alpha1"
|
||||
"github.com/crossplane/crossplane-runtime/pkg/fieldpath"
|
||||
"github.com/pkg/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/klog/v2"
|
||||
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha2"
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
|
||||
ctrlutil "github.com/oam-dev/kubevela/pkg/controller/utils"
|
||||
"github.com/oam-dev/kubevela/pkg/oam"
|
||||
"github.com/oam-dev/kubevela/pkg/oam/util"
|
||||
)
|
||||
|
||||
// NewAppManifests create a AppManifests
|
||||
func NewAppManifests(appRevision *v1beta1.ApplicationRevision) *AppManifests {
|
||||
return &AppManifests{AppRevision: appRevision}
|
||||
}
|
||||
|
||||
// AppManifests contains configuration to assemble resources recorded in the ApplicationRevision.
|
||||
// 'Assemble' means expand Application(Component and Trait) into K8s resource and get them ready to go, to be emitted
|
||||
// into K8s
|
||||
type AppManifests struct {
|
||||
AppRevision *v1beta1.ApplicationRevision
|
||||
WorkloadOptions []WorkloadOption
|
||||
|
||||
appComponents []applicationComponent
|
||||
components []*v1alpha2.Component
|
||||
|
||||
appName string
|
||||
appNamespace string
|
||||
appLabels map[string]string
|
||||
appAnnotations map[string]string
|
||||
appOwnerRef *metav1.OwnerReference
|
||||
|
||||
assembledWorkloads map[string]*unstructured.Unstructured
|
||||
assembledTraits map[string][]*unstructured.Unstructured
|
||||
// key is workload reference, values are the references of scopes the workload belongs to
|
||||
referencedScopes map[runtimev1alpha1.TypedReference][]runtimev1alpha1.TypedReference
|
||||
|
||||
finalized bool
|
||||
err error
|
||||
}
|
||||
|
||||
// this is a helper struct to replace v1alpha2.ApplicationConfiguration for this moment
|
||||
type applicationComponent struct {
|
||||
revisionName string
|
||||
traits []v1alpha2.ComponentTrait
|
||||
scopes []runtimev1alpha1.TypedReference
|
||||
}
|
||||
|
||||
// WorkloadOption will be applied to each workloads AFTER it has been assembled by generic rules shown below:
|
||||
// 1) use component name as workload name
|
||||
// 2) use application namespace as workload namespace if unspecified
|
||||
// 3) set application as workload's owner
|
||||
// 4) pass all application's labels and annotations to workload's
|
||||
// Component and ComponentDefinition are enough for caller to manipulate workloads.
|
||||
// Caller can use below labels of workload to get more information:
|
||||
// - oam.LabelAppName
|
||||
// - oam.LabelAppRevision
|
||||
// - oam.LabelAppRevisionHash
|
||||
// - oam.LabelAppComponent
|
||||
// - oam.LabelAppComponentRevision
|
||||
type WorkloadOption interface {
|
||||
ApplyToWorkload(workload *unstructured.Unstructured, comp *v1alpha2.Component, compDefinition *v1beta1.ComponentDefinition) error
|
||||
}
|
||||
|
||||
// WithWorkloadOption add a WorkloadOption to plug in custom logic applied to each workload
|
||||
func (am *AppManifests) WithWorkloadOption(wo WorkloadOption) *AppManifests {
|
||||
if am.WorkloadOptions == nil {
|
||||
am.WorkloadOptions = make([]WorkloadOption, 0)
|
||||
}
|
||||
am.WorkloadOptions = append(am.WorkloadOptions, wo)
|
||||
return am
|
||||
}
|
||||
|
||||
// AssembledManifests do assemble and merge all assembled resources(except referenced scopes) into one array
|
||||
func (am *AppManifests) AssembledManifests() ([]*unstructured.Unstructured, error) {
|
||||
if !am.finalized {
|
||||
am.assemble()
|
||||
}
|
||||
if am.err != nil {
|
||||
return nil, am.err
|
||||
}
|
||||
r := make([]*unstructured.Unstructured, 0)
|
||||
for _, wl := range am.assembledWorkloads {
|
||||
r = append(r, wl.DeepCopy())
|
||||
}
|
||||
for _, ts := range am.assembledTraits {
|
||||
for _, t := range ts {
|
||||
r = append(r, t.DeepCopy())
|
||||
}
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// ReferencedScopes do assemble and return workload reference and referenced scopes
|
||||
func (am *AppManifests) ReferencedScopes() (map[runtimev1alpha1.TypedReference][]runtimev1alpha1.TypedReference, error) {
|
||||
if !am.finalized {
|
||||
am.assemble()
|
||||
}
|
||||
if am.err != nil {
|
||||
return nil, am.err
|
||||
}
|
||||
r := make(map[runtimev1alpha1.TypedReference][]runtimev1alpha1.TypedReference)
|
||||
for k, refs := range am.referencedScopes {
|
||||
r[k] = make([]runtimev1alpha1.TypedReference, len(refs))
|
||||
copy(r[k], refs)
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// GroupAssembledManifests do assemble and return all resources grouped by components
|
||||
func (am *AppManifests) GroupAssembledManifests() (
|
||||
map[string]*unstructured.Unstructured,
|
||||
map[string][]*unstructured.Unstructured,
|
||||
map[runtimev1alpha1.TypedReference][]runtimev1alpha1.TypedReference, error) {
|
||||
if !am.finalized {
|
||||
am.assemble()
|
||||
}
|
||||
if am.err != nil {
|
||||
return nil, nil, nil, am.err
|
||||
}
|
||||
workloads := make(map[string]*unstructured.Unstructured)
|
||||
for k, wl := range am.assembledWorkloads {
|
||||
workloads[k] = wl.DeepCopy()
|
||||
}
|
||||
traits := make(map[string][]*unstructured.Unstructured)
|
||||
for k, ts := range am.assembledTraits {
|
||||
traits[k] = make([]*unstructured.Unstructured, len(ts))
|
||||
for i, t := range ts {
|
||||
traits[k][i] = t.DeepCopy()
|
||||
}
|
||||
}
|
||||
scopes := make(map[runtimev1alpha1.TypedReference][]runtimev1alpha1.TypedReference)
|
||||
for k, v := range am.referencedScopes {
|
||||
scopes[k] = make([]runtimev1alpha1.TypedReference, len(v))
|
||||
copy(scopes[k], v)
|
||||
}
|
||||
return workloads, traits, scopes, nil
|
||||
}
|
||||
|
||||
func (am *AppManifests) assemble() {
|
||||
am.complete()
|
||||
klog.InfoS("Assemble manifests for application", "name", am.appName, "revision", am.AppRevision.GetName())
|
||||
if err := am.validate(); err != nil {
|
||||
am.finalizeAssemble(err)
|
||||
return
|
||||
}
|
||||
for _, ac := range am.appComponents {
|
||||
compRevisionName := ac.revisionName
|
||||
compName := ctrlutil.ExtractComponentName(compRevisionName)
|
||||
commonLabels := am.generateCommonLabels(compName, compRevisionName)
|
||||
var workloadRef runtimev1alpha1.TypedReference
|
||||
klog.InfoS("Assemble manifests for component", "name", compName)
|
||||
for _, comp := range am.components {
|
||||
if comp.Name == compName {
|
||||
wl, err := am.assembleWorkload(comp, commonLabels)
|
||||
if err != nil {
|
||||
am.finalizeAssemble(err)
|
||||
return
|
||||
}
|
||||
am.assembledWorkloads[compName] = wl
|
||||
workloadRef = runtimev1alpha1.TypedReference{
|
||||
APIVersion: wl.GetAPIVersion(),
|
||||
Kind: wl.GetKind(),
|
||||
Name: wl.GetName(),
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
am.assembledTraits[compName] = make([]*unstructured.Unstructured, len(ac.traits))
|
||||
for i, compTrait := range ac.traits {
|
||||
trait, err := am.assembleTrait(compTrait, compName, commonLabels)
|
||||
if err != nil {
|
||||
am.finalizeAssemble(err)
|
||||
return
|
||||
}
|
||||
if err := am.setWorkloadRefToTrait(workloadRef, trait); err != nil {
|
||||
am.finalizeAssemble(errors.WithMessagef(err, "cannot set workload reference to trait %q", trait.GetName()))
|
||||
return
|
||||
}
|
||||
am.assembledTraits[compName][i] = trait
|
||||
}
|
||||
|
||||
am.referencedScopes[workloadRef] = make([]runtimev1alpha1.TypedReference, len(ac.scopes))
|
||||
for i, scope := range ac.scopes {
|
||||
am.referencedScopes[workloadRef][i] = scope
|
||||
}
|
||||
}
|
||||
am.finalizeAssemble(nil)
|
||||
}
|
||||
|
||||
func (am *AppManifests) complete() {
|
||||
// safe to skip error-check
|
||||
appConfig, _ := convertRawExtention2AppConfig(am.AppRevision.Spec.ApplicationConfiguration)
|
||||
// convert v1alpha2.ApplicationConfiguration to a helper struct
|
||||
am.appComponents = make([]applicationComponent, len(appConfig.Spec.Components))
|
||||
for i, acc := range appConfig.Spec.Components {
|
||||
am.appComponents[i] = applicationComponent{
|
||||
revisionName: acc.RevisionName,
|
||||
}
|
||||
am.appComponents[i].traits = make([]v1alpha2.ComponentTrait, len(acc.Traits))
|
||||
copy(am.appComponents[i].traits, acc.Traits)
|
||||
am.appComponents[i].scopes = make([]runtimev1alpha1.TypedReference, len(acc.Scopes))
|
||||
for j, s := range acc.Scopes {
|
||||
am.appComponents[i].scopes[j] = s.ScopeReference
|
||||
}
|
||||
}
|
||||
// Application entity in the ApplicationRevision has no metadata,
|
||||
// so we have to get below information from AppConfig.
|
||||
// Up-stream process must set these to AppConfig.
|
||||
am.appName = appConfig.GetName()
|
||||
am.appNamespace = appConfig.GetNamespace()
|
||||
am.appLabels = appConfig.GetLabels()
|
||||
am.appAnnotations = appConfig.GetAnnotations()
|
||||
am.appOwnerRef = metav1.GetControllerOf(appConfig)
|
||||
|
||||
am.components = make([]*v1alpha2.Component, len(am.AppRevision.Spec.Components))
|
||||
for i, rawComp := range am.AppRevision.Spec.Components {
|
||||
// safe to skip error-check
|
||||
comp, _ := convertRawExtention2Component(rawComp.Raw)
|
||||
am.components[i] = comp
|
||||
}
|
||||
|
||||
am.assembledWorkloads = make(map[string]*unstructured.Unstructured)
|
||||
am.assembledTraits = make(map[string][]*unstructured.Unstructured)
|
||||
am.referencedScopes = make(map[runtimev1alpha1.TypedReference][]runtimev1alpha1.TypedReference)
|
||||
}
|
||||
|
||||
func (am *AppManifests) finalizeAssemble(err error) {
|
||||
am.finalized = true
|
||||
if err != nil {
|
||||
klog.ErrorS(err, "Failed assembling manifests for application", "name", am.appName, "revision", am.AppRevision.GetName())
|
||||
am.err = errors.WithMessagef(err, "cannot assemble resources' manifests for application %q", am.appName)
|
||||
}
|
||||
klog.InfoS("Successfully assemble manifests for application", "name", am.appName, "revision", am.AppRevision.GetName(), "namespace", am.appNamespace)
|
||||
}
|
||||
|
||||
// AssembleOptions is highly coulped with AppRevision, should check the AppRevision provides all info
|
||||
// required by AssembleOptions
|
||||
func (am *AppManifests) validate() error {
|
||||
if am.appOwnerRef == nil {
|
||||
return errors.New("AppRevision must have an Application as owner")
|
||||
}
|
||||
if len(am.AppRevision.Labels[oam.LabelAppRevisionHash]) == 0 {
|
||||
return errors.New("AppRevision must have revision hash recorded in the label")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// workload and trait in the same component both have these labels
|
||||
func (am *AppManifests) generateCommonLabels(compName, compRevisionName string) map[string]string {
|
||||
Labels := map[string]string{
|
||||
oam.LabelAppName: am.appName,
|
||||
oam.LabelAppRevision: am.AppRevision.Name,
|
||||
oam.LabelAppRevisionHash: am.AppRevision.Labels[oam.LabelAppRevisionHash],
|
||||
oam.LabelAppComponent: compName,
|
||||
oam.LabelAppComponentRevision: compRevisionName,
|
||||
}
|
||||
// pass application's all labels to workload/trait
|
||||
return util.MergeMapOverrideWithDst(Labels, am.appLabels)
|
||||
}
|
||||
|
||||
// workload and trait both have these annotations
|
||||
func (am *AppManifests) setAnnotations(obj *unstructured.Unstructured) {
|
||||
// pass application's all annotations
|
||||
util.AddAnnotations(obj, am.appAnnotations)
|
||||
// remove useless annotations for workload/trait
|
||||
util.RemoveAnnotations(obj, []string{
|
||||
oam.AnnotationAppRollout,
|
||||
oam.AnnotationRollingComponent,
|
||||
oam.AnnotationInplaceUpgrade,
|
||||
})
|
||||
}
|
||||
|
||||
func (am *AppManifests) setNamespace(obj *unstructured.Unstructured) {
|
||||
// only set app's namespace when namespace is unspecified
|
||||
// it's by design to set arbitrary namespace in render phase
|
||||
if len(obj.GetNamespace()) == 0 {
|
||||
obj.SetNamespace(am.appNamespace)
|
||||
}
|
||||
}
|
||||
|
||||
func (am *AppManifests) setOwnerReference(obj *unstructured.Unstructured) {
|
||||
obj.SetOwnerReferences([]metav1.OwnerReference{*am.appOwnerRef})
|
||||
}
|
||||
|
||||
func (am *AppManifests) assembleWorkload(comp *v1alpha2.Component, labels map[string]string) (*unstructured.Unstructured, error) {
|
||||
compName := comp.Name
|
||||
wl, err := util.RawExtension2Unstructured(&comp.Spec.Workload)
|
||||
if err != nil {
|
||||
return nil, errors.WithMessagef(err, "cannot convert raw workload in component %q", compName)
|
||||
}
|
||||
// use component name as workload name
|
||||
// override the name set in render phase if exist
|
||||
wl.SetName(compName)
|
||||
am.setWorkloadLabels(wl, labels)
|
||||
am.setAnnotations(wl)
|
||||
am.setNamespace(wl)
|
||||
am.setOwnerReference(wl)
|
||||
|
||||
workloadType := wl.GetLabels()[oam.WorkloadTypeLabel]
|
||||
compDefinition := am.AppRevision.Spec.ComponentDefinitions[workloadType]
|
||||
for _, wo := range am.WorkloadOptions {
|
||||
if err := wo.ApplyToWorkload(wl, comp.DeepCopy(), compDefinition.DeepCopy()); err != nil {
|
||||
klog.ErrorS(err, "Failed applying a workload option", "workload", klog.KObj(wl), "name", wl.GetName())
|
||||
return nil, errors.Wrapf(err, "cannot apply workload option for component %q", compName)
|
||||
}
|
||||
klog.InfoS("Successfully apply a workload option", "workload", klog.KObj(wl), "name", wl.GetName())
|
||||
}
|
||||
klog.InfoS("Successfully assemble a workload", "workload", klog.KObj(wl), "APIVersion", wl.GetAPIVersion(), "Kind", wl.GetKind())
|
||||
return wl, nil
|
||||
}
|
||||
|
||||
func (am *AppManifests) setWorkloadLabels(wl *unstructured.Unstructured, commonLabels map[string]string) {
|
||||
// add more workload-specific labels here
|
||||
util.AddLabels(wl, map[string]string{oam.LabelOAMResourceType: oam.ResourceTypeWorkload})
|
||||
util.AddLabels(wl, commonLabels)
|
||||
|
||||
/* NOTE a workload has these possible labels
|
||||
app.oam.dev/app-revision-hash: ce053923e2fb403f
|
||||
app.oam.dev/appRevision: myapp-v2
|
||||
app.oam.dev/component: mycomp
|
||||
app.oam.dev/name: myapp
|
||||
app.oam.dev/resourceType: WORKLOAD
|
||||
app.oam.dev/revision: mycomp-v2
|
||||
workload.oam.dev/type: kube-worker
|
||||
*/
|
||||
}
|
||||
|
||||
func (am *AppManifests) assembleTrait(compTrait v1alpha2.ComponentTrait, compName string, labels map[string]string) (*unstructured.Unstructured, error) {
|
||||
trait, err := util.RawExtension2Unstructured(&compTrait.Trait)
|
||||
if err != nil {
|
||||
return nil, errors.WithMessagef(err, "cannot convert raw trait in component")
|
||||
}
|
||||
traitType := trait.GetLabels()[oam.TraitTypeLabel]
|
||||
// only set generated name when name is unspecified
|
||||
// it's by design to set arbitrary name in render phase
|
||||
if len(trait.GetName()) == 0 {
|
||||
traitName := util.GenTraitName(compName, &compTrait, traitType)
|
||||
trait.SetName(traitName)
|
||||
}
|
||||
am.setTraitLabels(trait, labels)
|
||||
am.setAnnotations(trait)
|
||||
am.setNamespace(trait)
|
||||
am.setOwnerReference(trait)
|
||||
klog.InfoS("Successfully assemble a trait", "trait", klog.KObj(trait), "APIVersion", trait.GetAPIVersion(), "Kind", trait.GetKind())
|
||||
return trait, nil
|
||||
}
|
||||
|
||||
func (am *AppManifests) setTraitLabels(trait *unstructured.Unstructured, commonLabels map[string]string) {
|
||||
// add more trait-specific labels here
|
||||
util.AddLabels(trait, map[string]string{oam.LabelOAMResourceType: oam.ResourceTypeTrait})
|
||||
util.AddLabels(trait, commonLabels)
|
||||
|
||||
/* NOTE a trait has these possible labels
|
||||
app.oam.dev/app-revision-hash: ce053923e2fb403f
|
||||
app.oam.dev/appRevision: myapp-v2
|
||||
app.oam.dev/component: mycomp
|
||||
app.oam.dev/name: myapp
|
||||
app.oam.dev/resourceType: TRAIT
|
||||
app.oam.dev/revision: mycomp-v2
|
||||
trait.oam.dev/resource: service
|
||||
trait.oam.dev/type: ingress // already added in render phase
|
||||
*/
|
||||
}
|
||||
|
||||
func (am *AppManifests) setWorkloadRefToTrait(wlRef runtimev1alpha1.TypedReference, trait *unstructured.Unstructured) error {
|
||||
traitType := trait.GetLabels()[oam.TraitTypeLabel]
|
||||
traitDef := am.AppRevision.Spec.TraitDefinitions[traitType]
|
||||
workloadRefPath := traitDef.Spec.WorkloadRefPath
|
||||
// only add workload reference to the trait if it asks for it
|
||||
if len(workloadRefPath) != 0 {
|
||||
if err := fieldpath.Pave(trait.UnstructuredContent()).SetValue(workloadRefPath, wlRef); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
/*
|
||||
Copyright 2021 The KubeVela 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 assemble
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
runtimev1alpha1 "github.com/crossplane/crossplane-runtime/apis/core/v1alpha1"
|
||||
"github.com/ghodss/yaml"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
|
||||
"github.com/oam-dev/kubevela/pkg/oam"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
func TestAssemble(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Assemble Suite")
|
||||
}
|
||||
|
||||
var _ = Describe("Test Assemble Options", func() {
|
||||
It("test assemble", func() {
|
||||
var (
|
||||
compName = "test-comp"
|
||||
namespace = "default"
|
||||
)
|
||||
|
||||
appRev := &v1beta1.ApplicationRevision{}
|
||||
b, err := ioutil.ReadFile("./testdata/apprevision.yaml")
|
||||
/* appRevision test data is generated based on below application
|
||||
apiVersion: core.oam.dev/v1beta1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: test-assemble
|
||||
spec:
|
||||
components:
|
||||
- name: test-comp
|
||||
type: webservice
|
||||
properties:
|
||||
image: crccheck/hello-world
|
||||
port: 8000
|
||||
traits:
|
||||
- type: ingress
|
||||
properties:
|
||||
domain: localhost
|
||||
http:
|
||||
"/": 8000
|
||||
- type: manualscaler
|
||||
properties:
|
||||
replicas: 3
|
||||
*/
|
||||
Expect(err).Should(BeNil())
|
||||
err = yaml.Unmarshal(b, appRev)
|
||||
Expect(err).Should(BeNil())
|
||||
|
||||
ao := NewAppManifests(appRev)
|
||||
workloads, traits, _, err := ao.GroupAssembledManifests()
|
||||
Expect(err).Should(BeNil())
|
||||
|
||||
By("Verify amount of result resources")
|
||||
allResources, err := ao.AssembledManifests()
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(len(allResources)).Should(Equal(4))
|
||||
|
||||
By("Verify amount of result grouped resources")
|
||||
Expect(len(workloads)).Should(Equal(1))
|
||||
Expect(len(traits[compName])).Should(Equal(3))
|
||||
|
||||
By("Verify workload metadata (name, namespace, labels, annotations, ownerRef)")
|
||||
wl := workloads[compName]
|
||||
Expect(wl.GetName()).Should(Equal(compName))
|
||||
Expect(wl.GetNamespace()).Should(Equal(namespace))
|
||||
labels := wl.GetLabels()
|
||||
labelKeys := make([]string, 0, len(labels))
|
||||
for k := range labels {
|
||||
labelKeys = append(labelKeys, k)
|
||||
}
|
||||
Expect(labelKeys).Should(ContainElements(
|
||||
oam.LabelAppName,
|
||||
oam.LabelAppRevision,
|
||||
oam.LabelAppRevisionHash,
|
||||
oam.LabelAppComponent,
|
||||
oam.LabelAppComponentRevision,
|
||||
oam.WorkloadTypeLabel,
|
||||
oam.LabelOAMResourceType))
|
||||
Expect(len(wl.GetAnnotations())).Should(Equal(1))
|
||||
ownerRef := metav1.GetControllerOf(wl)
|
||||
Expect(ownerRef.Kind).Should(Equal("Application"))
|
||||
|
||||
By("Verify trait metadata (name, namespace, labels, annotations, ownerRef)")
|
||||
trait := traits[compName][0]
|
||||
Expect(trait.GetName()).Should(ContainSubstring(compName))
|
||||
Expect(trait.GetNamespace()).Should(Equal(namespace))
|
||||
labels = trait.GetLabels()
|
||||
labelKeys = make([]string, 0, len(labels))
|
||||
for k := range labels {
|
||||
labelKeys = append(labelKeys, k)
|
||||
}
|
||||
Expect(labelKeys).Should(ContainElements(
|
||||
oam.LabelAppName,
|
||||
oam.LabelAppRevision,
|
||||
oam.LabelAppRevisionHash,
|
||||
oam.LabelAppComponent,
|
||||
oam.LabelAppComponentRevision,
|
||||
oam.TraitTypeLabel,
|
||||
oam.LabelOAMResourceType))
|
||||
Expect(len(wl.GetAnnotations())).Should(Equal(1))
|
||||
ownerRef = metav1.GetControllerOf(trait)
|
||||
Expect(ownerRef.Kind).Should(Equal("Application"))
|
||||
|
||||
By("Verify set workload reference to trait")
|
||||
scaler := traits[compName][2]
|
||||
wlRef, found, err := unstructured.NestedMap(scaler.Object, "spec", "workloadRef")
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(found).Should(BeTrue())
|
||||
wantWorkloadRef := map[string]interface{}{
|
||||
"apiVersion": "apps/v1",
|
||||
"kind": "Deployment",
|
||||
"name": compName,
|
||||
}
|
||||
Expect(wlRef).Should(Equal(wantWorkloadRef))
|
||||
|
||||
By("Verify referenced scopes")
|
||||
scopes, err := ao.ReferencedScopes()
|
||||
Expect(err).Should(BeNil())
|
||||
wlTypedRef := runtimev1alpha1.TypedReference{
|
||||
APIVersion: "apps/v1",
|
||||
Kind: "Deployment",
|
||||
Name: compName,
|
||||
}
|
||||
Expect(len(scopes[wlTypedRef]) > 0).Should(BeTrue())
|
||||
wlScope := scopes[wlTypedRef][0]
|
||||
wantScopeRef := runtimev1alpha1.TypedReference{
|
||||
APIVersion: "core.oam.dev/v1beta1",
|
||||
Kind: "HealthScope",
|
||||
Name: "sample-health-scope",
|
||||
}
|
||||
Expect(wlScope).Should(Equal(wantScopeRef))
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
Copyright 2021 The KubeVela 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 assemble
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha2"
|
||||
)
|
||||
|
||||
// TODO duplicate with PR 1708, should remove latter
|
||||
func convertRawExtention2AppConfig(raw runtime.RawExtension) (*v1alpha2.ApplicationConfiguration, error) {
|
||||
obj := &v1alpha2.ApplicationConfiguration{}
|
||||
b, err := raw.MarshalJSON()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := json.Unmarshal(b, obj); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return obj, nil
|
||||
}
|
||||
|
||||
func convertRawExtention2Component(raw runtime.RawExtension) (*v1alpha2.Component, error) {
|
||||
obj := &v1alpha2.Component{}
|
||||
b, err := raw.MarshalJSON()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := json.Unmarshal(b, obj); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return obj, nil
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
/*
|
||||
Copyright 2021 The KubeVela 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 assemble
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/crossplane/crossplane-runtime/pkg/fieldpath"
|
||||
kruisev1alpha1 "github.com/openkruise/kruise-api/apps/v1alpha1"
|
||||
"github.com/pkg/errors"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/klog/v2"
|
||||
"k8s.io/utils/pointer"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha2"
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
|
||||
helmapi "github.com/oam-dev/kubevela/pkg/appfile/helm/flux2apis"
|
||||
"github.com/oam-dev/kubevela/pkg/oam"
|
||||
"github.com/oam-dev/kubevela/pkg/oam/util"
|
||||
)
|
||||
|
||||
// WorkloadOptionFn implement interface WorkloadOption
|
||||
type WorkloadOptionFn func(*unstructured.Unstructured, *v1alpha2.Component, *v1beta1.ComponentDefinition) error
|
||||
|
||||
// ApplyToWorkload will apply the manipulation defined in the function to assembled workload
|
||||
func (fn WorkloadOptionFn) ApplyToWorkload(wl *unstructured.Unstructured, comp *v1alpha2.Component, compDefinition *v1beta1.ComponentDefinition) error {
|
||||
return fn(wl, comp, compDefinition)
|
||||
}
|
||||
|
||||
// DiscoveryHelmBasedWorkload only works for Helm-based component. It computes a qualifiedFullName for the workload and
|
||||
// try to get it from K8s cluster.
|
||||
// If not found, block down-streaming process until Helm creates the workload successfully.
|
||||
func DiscoveryHelmBasedWorkload(ctx context.Context, c client.Reader) WorkloadOption {
|
||||
return WorkloadOptionFn(func(assembledWorkload *unstructured.Unstructured, comp *v1alpha2.Component, _ *v1beta1.ComponentDefinition) error {
|
||||
return discoverHelmModuleWorkload(ctx, c, assembledWorkload, comp)
|
||||
})
|
||||
}
|
||||
|
||||
func discoverHelmModuleWorkload(ctx context.Context, c client.Reader, assembledWorkload *unstructured.Unstructured, comp *v1alpha2.Component) error {
|
||||
if comp == nil || comp.Spec.Helm == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
ns := assembledWorkload.GetNamespace()
|
||||
rls, err := util.RawExtension2Unstructured(&comp.Spec.Helm.Release)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "cannot get helm release from component")
|
||||
}
|
||||
rlsName := rls.GetName()
|
||||
|
||||
chartName, ok, err := unstructured.NestedString(rls.Object, helmapi.HelmChartNamePath...)
|
||||
if err != nil || !ok {
|
||||
return errors.New("cannot get helm chart name")
|
||||
}
|
||||
|
||||
// qualifiedFullName is used as the name of target workload.
|
||||
// It strictly follows the convention that Helm generate default full name as below:
|
||||
// > We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
|
||||
// > If release name contains chart name it will be used as a full name.
|
||||
qualifiedWorkloadName := rlsName
|
||||
if !strings.Contains(rlsName, chartName) {
|
||||
qualifiedWorkloadName = fmt.Sprintf("%s-%s", rlsName, chartName)
|
||||
if len(qualifiedWorkloadName) > 63 {
|
||||
qualifiedWorkloadName = strings.TrimSuffix(qualifiedWorkloadName[:63], "-")
|
||||
}
|
||||
}
|
||||
|
||||
workloadByHelm := &unstructured.Unstructured{}
|
||||
if err := c.Get(ctx, client.ObjectKey{Namespace: ns, Name: qualifiedWorkloadName}, workloadByHelm); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// check it's created by helm and match the release info
|
||||
annots := workloadByHelm.GetAnnotations()
|
||||
labels := workloadByHelm.GetLabels()
|
||||
if annots == nil || labels == nil ||
|
||||
annots["meta.helm.sh/release-name"] != rlsName ||
|
||||
annots["meta.helm.sh/release-namespace"] != ns ||
|
||||
labels["app.kubernetes.io/managed-by"] != "Helm" {
|
||||
err := fmt.Errorf("the workload is found but not match with helm info(meta.helm.sh/release-name: %s, meta.helm.sh/namespace: %s, app.kubernetes.io/managed-by: Helm)", rlsName, ns)
|
||||
klog.ErrorS(err, "Found a name-matched workload but not managed by Helm", "name", qualifiedWorkloadName,
|
||||
"annotations", annots, "labels", labels)
|
||||
return err
|
||||
}
|
||||
*assembledWorkload = *workloadByHelm
|
||||
return nil
|
||||
}
|
||||
|
||||
// NameNonInplaceUpgradableWorkload set workload name with component revision name to override component name.
|
||||
func NameNonInplaceUpgradableWorkload() WorkloadOption {
|
||||
return WorkloadOptionFn(func(wl *unstructured.Unstructured, comp *v1alpha2.Component, _ *v1beta1.ComponentDefinition) error {
|
||||
compRevName := wl.GetLabels()[oam.LabelAppComponentRevision]
|
||||
wl.SetName(compRevName)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// PrepareWorkloadForRollout prepare the workload before it is emit to the k8s. The current approach is to mark it
|
||||
// as disabled so that it's spec won't take effect immediately. The rollout controller can take over the resources
|
||||
// and enable it on its own since app controller here won't override their change
|
||||
func PrepareWorkloadForRollout() WorkloadOption {
|
||||
return WorkloadOptionFn(func(assembledWorkload *unstructured.Unstructured, _ *v1alpha2.Component, _ *v1beta1.ComponentDefinition) error {
|
||||
const (
|
||||
// below are the resources that we know how to disable
|
||||
cloneSetDisablePath = "spec.updateStrategy.paused"
|
||||
advancedStatefulSetDisablePath = "spec.updateStrategy.rollingUpdate.paused"
|
||||
deploymentDisablePath = "spec.paused"
|
||||
)
|
||||
// change the ownerReference and rollout controller will take it over
|
||||
ownerRef := metav1.GetControllerOf(assembledWorkload)
|
||||
ownerRef.Controller = pointer.BoolPtr(false)
|
||||
|
||||
pv := fieldpath.Pave(assembledWorkload.UnstructuredContent())
|
||||
|
||||
// TODO: we can get the workloadDefinition name from workload.GetLabels()["oam.WorkloadTypeLabel"]
|
||||
// and use a special field like "disablePath" in the definition to allow configurable behavior
|
||||
|
||||
// we hard code the behavior depends on the known assembledWorkload.group/kind for now.
|
||||
if assembledWorkload.GroupVersionKind().Group == kruisev1alpha1.GroupVersion.Group {
|
||||
switch assembledWorkload.GetKind() {
|
||||
case reflect.TypeOf(kruisev1alpha1.CloneSet{}).Name():
|
||||
err := pv.SetBool(cloneSetDisablePath, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
klog.InfoS("we render a CloneSet assembledWorkload.paused on the first time",
|
||||
"kind", assembledWorkload.GetKind(), "instance name", assembledWorkload.GetName())
|
||||
return nil
|
||||
case reflect.TypeOf(kruisev1alpha1.StatefulSet{}).Name():
|
||||
err := pv.SetBool(advancedStatefulSetDisablePath, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
klog.InfoS("we render an advanced statefulset assembledWorkload.paused on the first time",
|
||||
"kind", assembledWorkload.GetKind(), "instance name", assembledWorkload.GetName())
|
||||
return nil
|
||||
}
|
||||
} else if assembledWorkload.GroupVersionKind().Group == appsv1.GroupName &&
|
||||
assembledWorkload.GetKind() == reflect.TypeOf(appsv1.Deployment{}).Name() {
|
||||
err := pv.SetBool(deploymentDisablePath, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
klog.InfoS("we render a deployment assembledWorkload.paused on the first time",
|
||||
"kind", assembledWorkload.GetKind(), "instance name", assembledWorkload.GetName())
|
||||
return nil
|
||||
}
|
||||
|
||||
klog.InfoS("we encountered an unknown resource, we don't know how to prepare it",
|
||||
"GVK", assembledWorkload.GroupVersionKind().String(), "instance name", assembledWorkload.GetName())
|
||||
return fmt.Errorf("we do not know how to prepare `%s` as it has an unknown type %s", assembledWorkload.GetName(),
|
||||
assembledWorkload.GroupVersionKind().String())
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
/*
|
||||
Copyright 2021 The KubeVela 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 assemble
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"reflect"
|
||||
|
||||
"github.com/ghodss/yaml"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/ginkgo/extensions/table"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/crossplane/crossplane-runtime/pkg/test"
|
||||
"github.com/openkruise/kruise-api/apps/v1alpha1"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha2"
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
|
||||
helmapi "github.com/oam-dev/kubevela/pkg/appfile/helm/flux2apis"
|
||||
"github.com/oam-dev/kubevela/pkg/oam/util"
|
||||
)
|
||||
|
||||
var _ = Describe("Test WorkloadOption", func() {
|
||||
var (
|
||||
compName = "test-comp"
|
||||
compRevName = "test-comp-v1"
|
||||
|
||||
appRev *v1beta1.ApplicationRevision
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
appRev = &v1beta1.ApplicationRevision{}
|
||||
b, err := ioutil.ReadFile("./testdata/apprevision.yaml")
|
||||
Expect(err).Should(BeNil())
|
||||
err = yaml.Unmarshal(b, appRev)
|
||||
Expect(err).Should(BeNil())
|
||||
})
|
||||
|
||||
It("test NameNonInplaceUpgradableWorkload WorkloadOption", func() {
|
||||
By("Add NameNonInplaceUpgradableWorkload workload option")
|
||||
ao := NewAppManifests(appRev).WithWorkloadOption(NameNonInplaceUpgradableWorkload())
|
||||
workloads, _, _, err := ao.GroupAssembledManifests()
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(len(workloads)).Should(Equal(1))
|
||||
|
||||
By("Verify workload name is set as component revision name")
|
||||
wl := workloads[compName]
|
||||
Expect(wl.GetName()).Should(Equal(compRevName))
|
||||
})
|
||||
|
||||
Context("test PrepareWorkloadForRollout WorkloadOption", func() {
|
||||
It("test rollout OpenKruise CloneSet", func() {
|
||||
By("Use openkruise CloneSet as workload")
|
||||
cs := v1alpha1.CloneSet{}
|
||||
cs.SetGroupVersionKind(v1alpha1.SchemeGroupVersion.WithKind(reflect.TypeOf(v1alpha1.CloneSet{}).Name()))
|
||||
comp := v1alpha2.Component{}
|
||||
comp.SetName(compName)
|
||||
comp.Spec.Workload = util.Object2RawExtension(cs)
|
||||
Expect(len(appRev.Spec.Components) > 0).Should(BeTrue())
|
||||
appRev.Spec.Components[0] = common.RawComponent{
|
||||
Raw: util.Object2RawExtension(comp),
|
||||
}
|
||||
|
||||
By("Add PrepareWorkloadForRollout WorkloadOption")
|
||||
ao := NewAppManifests(appRev).WithWorkloadOption(PrepareWorkloadForRollout())
|
||||
workloads, _, _, err := ao.GroupAssembledManifests()
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(len(workloads)).Should(Equal(1))
|
||||
|
||||
By("Verify workload name is set as component name")
|
||||
wl := workloads[compName]
|
||||
Expect(wl.GetName()).Should(Equal(compName))
|
||||
By("Verify workload is paused")
|
||||
assembledCS := &v1alpha1.CloneSet{}
|
||||
runtime.DefaultUnstructuredConverter.FromUnstructured(wl.Object, assembledCS)
|
||||
Expect(assembledCS.Spec.UpdateStrategy.Paused).Should(BeTrue())
|
||||
})
|
||||
|
||||
It("test rollout OpenKruise StatefulSet", func() {
|
||||
By("Use openkruise CloneSet as workload")
|
||||
sts := v1alpha1.StatefulSet{}
|
||||
sts.SetGroupVersionKind(v1alpha1.SchemeGroupVersion.WithKind(reflect.TypeOf(v1alpha1.StatefulSet{}).Name()))
|
||||
comp := v1alpha2.Component{}
|
||||
comp.SetName(compName)
|
||||
comp.Spec.Workload = util.Object2RawExtension(sts)
|
||||
Expect(len(appRev.Spec.Components) > 0).Should(BeTrue())
|
||||
appRev.Spec.Components[0] = common.RawComponent{
|
||||
Raw: util.Object2RawExtension(comp),
|
||||
}
|
||||
|
||||
By("Add PrepareWorkloadForRollout WorkloadOption")
|
||||
ao := NewAppManifests(appRev).WithWorkloadOption(PrepareWorkloadForRollout())
|
||||
workloads, _, _, err := ao.GroupAssembledManifests()
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(len(workloads)).Should(Equal(1))
|
||||
|
||||
By("Verify workload name is set as component name")
|
||||
wl := workloads[compName]
|
||||
Expect(wl.GetName()).Should(Equal(compName))
|
||||
By("Verify workload is paused")
|
||||
assembledCS := &v1alpha1.StatefulSet{}
|
||||
runtime.DefaultUnstructuredConverter.FromUnstructured(wl.Object, assembledCS)
|
||||
Expect(assembledCS.Spec.UpdateStrategy.RollingUpdate.Paused).Should(BeTrue())
|
||||
})
|
||||
|
||||
It("test rollout Deployment", func() {
|
||||
By("Add PrepareWorkloadForRollout WorkloadOption")
|
||||
ao := NewAppManifests(appRev).WithWorkloadOption(PrepareWorkloadForRollout())
|
||||
workloads, _, _, err := ao.GroupAssembledManifests()
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(len(workloads)).Should(Equal(1))
|
||||
|
||||
By("Verify workload name is set as component name")
|
||||
wl := workloads[compName]
|
||||
Expect(wl.GetName()).Should(Equal(compName))
|
||||
By("Verify workload is paused")
|
||||
assembledDeploy := &appsv1.Deployment{}
|
||||
runtime.DefaultUnstructuredConverter.FromUnstructured(wl.Object, assembledDeploy)
|
||||
Expect(assembledDeploy.Spec.Paused).Should(BeTrue())
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
Describe("test DiscoveryHelmBasedWorkload", func() {
|
||||
ns := "test-ns"
|
||||
releaseName := "test-rls"
|
||||
chartName := "test-chart"
|
||||
release := &unstructured.Unstructured{}
|
||||
release.SetGroupVersionKind(helmapi.HelmReleaseGVK)
|
||||
release.SetName(releaseName)
|
||||
unstructured.SetNestedMap(release.Object, map[string]interface{}{
|
||||
"chart": map[string]interface{}{
|
||||
"spec": map[string]interface{}{
|
||||
"chart": chartName,
|
||||
"version": "1.0.0",
|
||||
},
|
||||
},
|
||||
}, "spec")
|
||||
releaseRaw, _ := release.MarshalJSON()
|
||||
|
||||
rlsWithoutChart := release.DeepCopy()
|
||||
unstructured.SetNestedMap(rlsWithoutChart.Object, nil, "spec", "chart")
|
||||
rlsWithoutChartRaw, _ := rlsWithoutChart.MarshalJSON()
|
||||
|
||||
wl := &unstructured.Unstructured{}
|
||||
wl.SetLabels(map[string]string{
|
||||
"app.kubernetes.io/managed-by": "Helm",
|
||||
})
|
||||
wl.SetAnnotations(map[string]string{
|
||||
"meta.helm.sh/release-name": releaseName,
|
||||
"meta.helm.sh/release-namespace": ns,
|
||||
})
|
||||
type SubCase struct {
|
||||
reason string
|
||||
c client.Reader
|
||||
helm *common.Helm
|
||||
workloadInComp *unstructured.Unstructured
|
||||
wantWorkload *unstructured.Unstructured
|
||||
wantErr error
|
||||
}
|
||||
|
||||
DescribeTable("Test cases for DiscoveryHelmBasedWorkload", func(tc SubCase) {
|
||||
By(tc.reason)
|
||||
comp := &v1alpha2.Component{}
|
||||
if tc.workloadInComp != nil {
|
||||
wlRaw, _ := tc.workloadInComp.MarshalJSON()
|
||||
comp.Spec.Workload = runtime.RawExtension{Raw: wlRaw}
|
||||
}
|
||||
comp.Spec.Helm = tc.helm
|
||||
assembledWorkload := &unstructured.Unstructured{}
|
||||
assembledWorkload.SetNamespace(ns)
|
||||
err := discoverHelmModuleWorkload(context.Background(), tc.c, assembledWorkload, comp)
|
||||
|
||||
By("Verify error")
|
||||
diff := cmp.Diff(tc.wantErr, err, test.EquateErrors())
|
||||
Expect(diff).Should(BeEmpty())
|
||||
|
||||
if tc.wantErr == nil {
|
||||
By("Verify found workload")
|
||||
diff = cmp.Diff(tc.wantWorkload, assembledWorkload)
|
||||
Expect(diff).Should(BeEmpty())
|
||||
}
|
||||
},
|
||||
Entry("CannotGetReleaseFromComp", SubCase{
|
||||
reason: "An error should occur because cannot get release",
|
||||
helm: &common.Helm{
|
||||
Release: runtime.RawExtension{Raw: []byte("boom")},
|
||||
},
|
||||
wantErr: errors.Wrap(errors.New("invalid character 'b' looking for beginning of value"),
|
||||
"cannot get helm release from component"),
|
||||
}),
|
||||
Entry("CannotGetChartFromRelease", SubCase{
|
||||
reason: "An error should occur because cannot get chart info",
|
||||
helm: &common.Helm{
|
||||
Release: runtime.RawExtension{Raw: rlsWithoutChartRaw},
|
||||
},
|
||||
wantErr: errors.New("cannot get helm chart name"),
|
||||
}),
|
||||
Entry("CannotGetWorkload", SubCase{
|
||||
reason: "An error should occur because cannot get workload from k8s cluster",
|
||||
helm: &common.Helm{
|
||||
Release: runtime.RawExtension{Raw: releaseRaw},
|
||||
},
|
||||
workloadInComp: &unstructured.Unstructured{},
|
||||
c: &test.MockClient{MockGet: test.NewMockGetFn(errors.New("boom"))},
|
||||
wantErr: errors.New("boom"),
|
||||
}),
|
||||
Entry("GetNotMatchedWorkload", SubCase{
|
||||
reason: "An error should occur because the found workload is not managed by Helm",
|
||||
helm: &common.Helm{
|
||||
Release: runtime.RawExtension{Raw: releaseRaw},
|
||||
},
|
||||
workloadInComp: &unstructured.Unstructured{},
|
||||
c: &test.MockClient{MockGet: test.NewMockGetFn(nil, func(obj runtime.Object) error {
|
||||
o, _ := obj.(*unstructured.Unstructured)
|
||||
*o = unstructured.Unstructured{}
|
||||
o.SetLabels(map[string]string{
|
||||
"app.kubernetes.io/managed-by": "non-helm",
|
||||
})
|
||||
return nil
|
||||
})},
|
||||
wantErr: fmt.Errorf("the workload is found but not match with helm info(meta.helm.sh/release-name: %s,"+
|
||||
" meta.helm.sh/namespace: %s, app.kubernetes.io/managed-by: Helm)", "test-rls", "test-ns"),
|
||||
}),
|
||||
Entry("DiscoverSuccessfully", SubCase{
|
||||
reason: "No error should occur and the workload should be found",
|
||||
c: &test.MockClient{MockGet: test.NewMockGetFn(nil, func(obj runtime.Object) error {
|
||||
o, _ := obj.(*unstructured.Unstructured)
|
||||
*o = *wl.DeepCopy()
|
||||
return nil
|
||||
})},
|
||||
workloadInComp: wl.DeepCopy(),
|
||||
helm: &common.Helm{
|
||||
Release: runtime.RawExtension{Raw: releaseRaw},
|
||||
},
|
||||
wantWorkload: wl.DeepCopy(),
|
||||
wantErr: nil,
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
+299
@@ -0,0 +1,299 @@
|
||||
apiVersion: core.oam.dev/v1beta1
|
||||
kind: ApplicationRevision
|
||||
metadata:
|
||||
labels:
|
||||
app.oam.dev/app-revision-hash: f14d04f6c3210336
|
||||
app.oam.dev/name: test-assemble
|
||||
name: test-assemble-v1
|
||||
namespace: default
|
||||
ownerReferences:
|
||||
- apiVersion: core.oam.dev/v1beta1
|
||||
controller: false
|
||||
kind: Application
|
||||
name: test-assemble
|
||||
uid: e3b6afbe-2da6-49b6-bebe-63f0d46e8163
|
||||
spec:
|
||||
application:
|
||||
apiVersion: core.oam.dev/v1beta1
|
||||
kind: Application
|
||||
metadata: {}
|
||||
spec:
|
||||
components:
|
||||
- name: test-comp
|
||||
type: webservice
|
||||
properties:
|
||||
image: crccheck/hello-world
|
||||
port: 8000
|
||||
traits:
|
||||
- properties:
|
||||
domain: localhost
|
||||
http:
|
||||
/: 8000
|
||||
type: ingress
|
||||
- properties:
|
||||
replicas: 3
|
||||
type: manualscaler
|
||||
status:
|
||||
rollout:
|
||||
batchRollingState: ""
|
||||
currentBatch: 0
|
||||
lastTargetAppRevision: ""
|
||||
rollingState: ""
|
||||
upgradedReadyReplicas: 0
|
||||
upgradedReplicas: 0
|
||||
applicationConfiguration:
|
||||
apiVersion: core.oam.dev/v1alpha2
|
||||
kind: ApplicationConfiguration
|
||||
metadata:
|
||||
annotations:
|
||||
test.assemble: test
|
||||
labels:
|
||||
app.oam.dev/name: test-assemble
|
||||
name: test-assemble
|
||||
namespace: default
|
||||
ownerReferences:
|
||||
- apiVersion: core.oam.dev/v1beta1
|
||||
controller: true
|
||||
kind: Application
|
||||
name: test-assemble
|
||||
uid: e3b6afbe-2da6-49b6-bebe-63f0d46e8163
|
||||
spec:
|
||||
components:
|
||||
- revisionName: test-comp-v1
|
||||
scopes:
|
||||
- scopeRef:
|
||||
apiVersion: core.oam.dev/v1beta1
|
||||
kind: HealthScope
|
||||
name: sample-health-scope
|
||||
traits:
|
||||
- trait:
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
labels:
|
||||
app.oam.dev/appRevision: test-assemble-v1
|
||||
app.oam.dev/component: test-comp
|
||||
app.oam.dev/name: test-assemble
|
||||
trait.oam.dev/resource: service
|
||||
trait.oam.dev/type: ingress
|
||||
spec:
|
||||
ports:
|
||||
- port: 8000
|
||||
targetPort: 8000
|
||||
selector:
|
||||
app.oam.dev/component: test-comp
|
||||
- trait:
|
||||
apiVersion: networking.k8s.io/v1beta1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
labels:
|
||||
app.oam.dev/appRevision: test-assemble-v1
|
||||
app.oam.dev/component: test-comp
|
||||
app.oam.dev/name: test-assemble
|
||||
trait.oam.dev/resource: ingress
|
||||
trait.oam.dev/type: ingress
|
||||
name: test-comp
|
||||
spec:
|
||||
rules:
|
||||
- host: localhost
|
||||
http:
|
||||
paths:
|
||||
- backend:
|
||||
serviceName: test-comp
|
||||
servicePort: 8000
|
||||
path: /
|
||||
- trait:
|
||||
apiVersion: core.oam.dev/v1beta1
|
||||
kind: ManualScalerTrait
|
||||
metadata:
|
||||
labels:
|
||||
app.oam.dev/appRevision: test-assemble-v1
|
||||
app.oam.dev/component: test-comp
|
||||
app.oam.dev/name: test-assemble
|
||||
trait.oam.dev/resource: manualscaler
|
||||
trait.oam.dev/type: manualscaler
|
||||
name: test-comp
|
||||
spec:
|
||||
replicas: 3
|
||||
status:
|
||||
dependency: {}
|
||||
observedGeneration: 0
|
||||
componentDefinitions:
|
||||
webservice:
|
||||
apiVersion: core.oam.dev/v1beta1
|
||||
kind: ComponentDefinition
|
||||
metadata: {}
|
||||
spec:
|
||||
schematic:
|
||||
cue:
|
||||
template: "output: {\n\tapiVersion: \"apps/v1\"\n\tkind: \"Deployment\"\n\tspec:
|
||||
{\n\t\tselector: matchLabels: {\n\t\t\t\"app.oam.dev/component\": context.name\n\t\t\tif
|
||||
parameter.addRevisionLabel {\n\t\t\t\t\"app.oam.dev/appRevision\": context.appRevision\n\t\t\t}\n\t\t}\n\n\t\ttemplate:
|
||||
{\n\t\t\tmetadata: labels: {\n\t\t\t\t\"app.oam.dev/component\": context.name\n\t\t\t\tif
|
||||
parameter.addRevisionLabel {\n\t\t\t\t\t\"app.oam.dev/appRevision\":
|
||||
context.appRevision\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tspec: {\n\t\t\t\tcontainers:
|
||||
[{\n\t\t\t\t\tname: context.name\n\t\t\t\t\timage: parameter.image\n\n\t\t\t\t\tif
|
||||
parameter[\"cmd\"] != _|_ {\n\t\t\t\t\t\tcommand: parameter.cmd\n\t\t\t\t\t}\n\n\t\t\t\t\tif
|
||||
parameter[\"env\"] != _|_ {\n\t\t\t\t\t\tenv: parameter.env\n\t\t\t\t\t}\n\n\t\t\t\t\tif
|
||||
context[\"config\"] != _|_ {\n\t\t\t\t\t\tenv: context.config\n\t\t\t\t\t}\n\n\t\t\t\t\tports:
|
||||
[{\n\t\t\t\t\t\tcontainerPort: parameter.port\n\t\t\t\t\t}]\n\n\t\t\t\t\tif
|
||||
parameter[\"cpu\"] != _|_ {\n\t\t\t\t\t\tresources: {\n\t\t\t\t\t\t\tlimits:\n\t\t\t\t\t\t\t\tcpu:
|
||||
parameter.cpu\n\t\t\t\t\t\t\trequests:\n\t\t\t\t\t\t\t\tcpu: parameter.cpu\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tif
|
||||
parameter[\"volumes\"] != _|_ {\n\t\t\t\t\t\tvolumeMounts: [ for v in
|
||||
parameter.volumes {\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tmountPath: v.mountPath\n\t\t\t\t\t\t\t\tname:
|
||||
\ v.name\n\t\t\t\t\t\t\t}}]\n\t\t\t\t\t}\n\t\t\t\t}]\n\n\t\t\tif
|
||||
parameter[\"volumes\"] != _|_ {\n\t\t\t\tvolumes: [ for v in parameter.volumes
|
||||
{\n\t\t\t\t\t{\n\t\t\t\t\t\tname: v.name\n\t\t\t\t\t\tif v.type == \"pvc\"
|
||||
{\n\t\t\t\t\t\t\tpersistentVolumeClaim: {\n\t\t\t\t\t\t\t\tclaimName:
|
||||
v.claimName\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif v.type ==
|
||||
\"configMap\" {\n\t\t\t\t\t\t\tconfigMap: {\n\t\t\t\t\t\t\t\tdefaultMode:
|
||||
v.defaultMode\n\t\t\t\t\t\t\t\tname: v.cmName\n\t\t\t\t\t\t\t\tif
|
||||
v.items != _|_ {\n\t\t\t\t\t\t\t\t\titems: v.items\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif
|
||||
v.type == \"secret\" {\n\t\t\t\t\t\t\tsecret: {\n\t\t\t\t\t\t\t\tdefaultMode:
|
||||
v.defaultMode\n\t\t\t\t\t\t\t\tsecretName: v.secretName\n\t\t\t\t\t\t\t\tif
|
||||
v.items != _|_ {\n\t\t\t\t\t\t\t\t\titems: v.items\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif
|
||||
v.type == \"emptyDir\" {\n\t\t\t\t\t\t\temptyDir: {\n\t\t\t\t\t\t\t\tmedium:
|
||||
v.medium\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}}]\n\t\t\t}\n\t\t}\n\t\t}\n\t}\n}\nparameter:
|
||||
{\n\t// +usage=Which image would you like to use for your service\n\t//
|
||||
+short=i\n\timage: string\n\n\t// +usage=Commands to run in the container\n\tcmd?:
|
||||
[...string[]\n\n\t// +usage=Which port do you want customer traffic sent
|
||||
to\n\t// +short=p\n\tport: *80 | int\n\t// +usage=Define arguments by
|
||||
using environment variables\n\tenv?: [...{\n\t\t// +usage=Environment
|
||||
variable name\n\t\tname: string\n\t\t// +usage=The value of the environment
|
||||
variable\n\t\tvalue?: string\n\t\t// +usage=Specifies a source the value
|
||||
of this var should come from\n\t\tvalueFrom?: {\n\t\t\t// +usage=Selects
|
||||
a key of a secret in the pod's namespace\n\t\t\tsecretKeyRef: {\n\t\t\t\t//
|
||||
+usage=The name of the secret in the pod's namespace to select from\n\t\t\t\tname:
|
||||
string\n\t\t\t\t// +usage=The key of the secret to select from. Must
|
||||
be a valid secret key\n\t\t\t\tkey: string\n\t\t\t}\n\t\t}\n\t}]\n\t//
|
||||
+usage=Number of CPU units for the service, like `0.5` (0.5 CPU core),
|
||||
`1` (1 CPU core)\n\tcpu?: string\n\n\t// If addRevisionLabel is true,
|
||||
the appRevision label will be added to the underlying pods \n\taddRevisionLabel:
|
||||
*false | bool\n\n\t// +usage=Declare volumes and volumeMounts\n\tvolumes?:
|
||||
[...{\n\t\tname: string\n\t\tmountPath: string\n\t\t// +usage=Specify
|
||||
volume type, options: \"pvc\",\"configMap\",\"secret\",\"emptyDir\"\n\t\ttype:
|
||||
\"pvc\" | \"configMap\" | \"secret\" | \"emptyDir\"\n\t\tif type ==
|
||||
\"pvc\" {\n\t\t\tclaimName: string\n\t\t}\n\t\tif type == \"configMap\"
|
||||
{\n\t\t\tdefaultMode: *420 | int\n\t\t\tcmName: string\n\t\t\titems?:
|
||||
[...{\n\t\t\t\tkey: string\n\t\t\t\tpath: string\n\t\t\t\tmode: *511
|
||||
| int\n\t\t\t}]\n\t\t}\n\t\tif type == \"secret\" {\n\t\t\tdefaultMode:
|
||||
*420 | int\n\t\t\tsecretName: string\n\t\t\titems?: [...{\n\t\t\t\tkey:
|
||||
\ string\n\t\t\t\tpath: string\n\t\t\t\tmode: *511 | int\n\t\t\t}]\n\t\t}\n\t\tif
|
||||
type == \"emptyDir\" {\n\t\t\tmedium: *\"\" | \"Memory\"\n\t\t}\n\t}]\n}\n"
|
||||
workload:
|
||||
definition:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
status: {}
|
||||
components:
|
||||
- raw:
|
||||
apiVersion: core.oam.dev/v1alpha2
|
||||
kind: Component
|
||||
metadata:
|
||||
labels:
|
||||
app.oam.dev/name: test-assemble
|
||||
name: test-comp
|
||||
namespace: default
|
||||
ownerReferences:
|
||||
- apiVersion: core.oam.dev/v1beta1
|
||||
controller: true
|
||||
kind: Application
|
||||
name: test-assemble
|
||||
uid: e3b6afbe-2da6-49b6-bebe-63f0d46e8163
|
||||
spec:
|
||||
workload:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
labels:
|
||||
app.oam.dev/appRevision: test-assemble-v1
|
||||
app.oam.dev/component: test-comp
|
||||
app.oam.dev/name: test-assemble
|
||||
workload.oam.dev/type: webservice
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app.oam.dev/component: test-comp
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.oam.dev/component: test-comp
|
||||
spec:
|
||||
containers:
|
||||
- image: crccheck/hello-world
|
||||
name: test-comp
|
||||
ports:
|
||||
- containerPort: 8000
|
||||
status:
|
||||
observedGeneration: 0
|
||||
traitDefinitions:
|
||||
manualscaler:
|
||||
apiVersion: core.oam.dev/v1beta1
|
||||
kind: TraitDefinition
|
||||
metadata:
|
||||
annotations:
|
||||
definition.oam.dev/description: "Configures replicas for your service implemented by CRD controller."
|
||||
name: manualscaler
|
||||
spec:
|
||||
appliesToWorkloads:
|
||||
- deployments.apps
|
||||
definitionRef:
|
||||
name: manualscalertraits.core.oam.dev
|
||||
workloadRefPath: spec.workloadRef
|
||||
podDisruptive: true
|
||||
schematic:
|
||||
cue:
|
||||
template: |
|
||||
outputs: scaler: {
|
||||
apiVersion: "core.oam.dev/v1alpha2"
|
||||
kind: "ManualScalerTrait"
|
||||
spec: {
|
||||
replicaCount: parameter.replicas
|
||||
}
|
||||
}
|
||||
parameter: {
|
||||
//+short=r
|
||||
//+usage=Replicas of the workload
|
||||
replicas: *1 | int
|
||||
}
|
||||
ingress:
|
||||
apiVersion: core.oam.dev/v1beta1
|
||||
kind: TraitDefinition
|
||||
metadata: {}
|
||||
spec:
|
||||
appliesToWorkloads:
|
||||
- deployments.apps
|
||||
definitionRef:
|
||||
name: ""
|
||||
schematic:
|
||||
cue:
|
||||
template: "// trait template can have multiple outputs in one trait\noutputs:
|
||||
service: {\n\tapiVersion: \"v1\"\n\tkind: \"Service\"\n\tmetadata:\n\t\tname:
|
||||
context.name\n\tspec: {\n\t\tselector: {\n\t\t\t\"app.oam.dev/component\":
|
||||
context.name\n\t\t}\n\t\tports: [\n\t\t\tfor k, v in parameter.http
|
||||
{\n\t\t\t\tport: v\n\t\t\t\ttargetPort: v\n\t\t\t},\n\t\t]\n\t}\n}\n\noutputs:
|
||||
ingress: {\n\tapiVersion: \"networking.k8s.io/v1beta1\"\n\tkind: \"Ingress\"\n\tmetadata:\n\t\tname:
|
||||
context.name\n\tspec: {\n\t\trules: [{\n\t\t\thost: parameter.domain\n\t\t\thttp:
|
||||
{\n\t\t\t\tpaths: [\n\t\t\t\t\tfor k, v in parameter.http {\n\t\t\t\t\t\tpath:
|
||||
k\n\t\t\t\t\t\tbackend: {\n\t\t\t\t\t\t\tserviceName: context.name\n\t\t\t\t\t\t\tservicePort:
|
||||
v\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t]\n\t\t\t}\n\t\t}]\n\t}\n}\n\nparameter:
|
||||
{\n\t// +usage=Specify the domain you want to expose\n\tdomain: string\n\n\t//
|
||||
+usage=Specify the mapping relationship between the http path and the
|
||||
workload port\n\thttp: [string[]: int\n}\n"
|
||||
status:
|
||||
customStatus: |-
|
||||
let igs = context.outputs.ingress.status.loadBalancer.ingress
|
||||
if igs == _|_ {
|
||||
message: "No loadBalancer found, visiting by using 'vela port-forward " + context.appName + " --route'\n"
|
||||
}
|
||||
if len(igs) > 0 {
|
||||
if igs[0[].ip != _|_ {
|
||||
message: "Visiting URL: " + context.outputs.ingress.spec.rules[0[].host + ", IP: " + igs[0[].ip
|
||||
}
|
||||
if igs[0[].ip == _|_ {
|
||||
message: "Visiting URL: " + context.outputs.ingress.spec.rules[0[].host
|
||||
}
|
||||
}
|
||||
healthPolicy: |
|
||||
isHealth: len(context.outputs.service.spec.clusterIP) > 0
|
||||
status: {}
|
||||
Reference in New Issue
Block a user