diff --git a/pkg/controller/core.oam.dev/v1alpha2/application/assemble/assemble.go b/pkg/controller/core.oam.dev/v1alpha2/application/assemble/assemble.go new file mode 100644 index 000000000..0fc7b9dd2 --- /dev/null +++ b/pkg/controller/core.oam.dev/v1alpha2/application/assemble/assemble.go @@ -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 +} diff --git a/pkg/controller/core.oam.dev/v1alpha2/application/assemble/assemble_suite_test.go b/pkg/controller/core.oam.dev/v1alpha2/application/assemble/assemble_suite_test.go new file mode 100644 index 000000000..f2d94cb55 --- /dev/null +++ b/pkg/controller/core.oam.dev/v1alpha2/application/assemble/assemble_suite_test.go @@ -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)) + }) +}) diff --git a/pkg/controller/core.oam.dev/v1alpha2/application/assemble/helper.go b/pkg/controller/core.oam.dev/v1alpha2/application/assemble/helper.go new file mode 100644 index 000000000..5b7f453d5 --- /dev/null +++ b/pkg/controller/core.oam.dev/v1alpha2/application/assemble/helper.go @@ -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 +} diff --git a/pkg/controller/core.oam.dev/v1alpha2/application/assemble/options.go b/pkg/controller/core.oam.dev/v1alpha2/application/assemble/options.go new file mode 100644 index 000000000..1157fe174 --- /dev/null +++ b/pkg/controller/core.oam.dev/v1alpha2/application/assemble/options.go @@ -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()) + }) +} diff --git a/pkg/controller/core.oam.dev/v1alpha2/application/assemble/options_test.go b/pkg/controller/core.oam.dev/v1alpha2/application/assemble/options_test.go new file mode 100644 index 000000000..13489b9e0 --- /dev/null +++ b/pkg/controller/core.oam.dev/v1alpha2/application/assemble/options_test.go @@ -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, + }), + ) + }) +}) diff --git a/pkg/controller/core.oam.dev/v1alpha2/application/assemble/testdata/apprevision.yaml b/pkg/controller/core.oam.dev/v1alpha2/application/assemble/testdata/apprevision.yaml new file mode 100644 index 000000000..fe4b60546 --- /dev/null +++ b/pkg/controller/core.oam.dev/v1alpha2/application/assemble/testdata/apprevision.yaml @@ -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: {}