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:
Yue Wang
2021-06-01 16:34:25 +08:00
committed by GitHub
parent 931ca3ea3e
commit 21fe5fab9e
6 changed files with 1346 additions and 0 deletions
@@ -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,
}),
)
})
})
@@ -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: {}