diff --git a/apis/types/componentmanifest.go b/apis/types/componentmanifest.go index 4bb94afe7..2b724f653 100644 --- a/apis/types/componentmanifest.go +++ b/apis/types/componentmanifest.go @@ -34,4 +34,9 @@ type ComponentManifest struct { // Release, Git Repo or anything that can package and run a workload. PackagedWorkloadResources []*unstructured.Unstructured PackagedTraitResources map[string][]*unstructured.Unstructured + + // InsertConfigNotReady is true indicates the ComponentManifest is not ready to apply for insertSecret and configs + // it's possible for some of the component not ready while others are ready, we should not block all of them if only + // part is not ready + InsertConfigNotReady bool } diff --git a/docs/examples/insert-secret/app.yaml b/docs/examples/insert-secret/app.yaml new file mode 100644 index 000000000..ea7afd5bf --- /dev/null +++ b/docs/examples/insert-secret/app.yaml @@ -0,0 +1,19 @@ +apiVersion: core.oam.dev/v1beta1 +kind: Application +metadata: + name: myapp + namespace: default +spec: + components: + - name: comp1 + type: webservice + properties: + image: busybox + cmd: + - "sleep" + - "1000" + - name: myweb + type: secretconsumer + properties: + image: nginx:1.14.0 + dbSecret: mys \ No newline at end of file diff --git a/docs/examples/insert-secret/insertsecret.yaml b/docs/examples/insert-secret/insertsecret.yaml new file mode 100644 index 000000000..1c9975d21 --- /dev/null +++ b/docs/examples/insert-secret/insertsecret.yaml @@ -0,0 +1,59 @@ +apiVersion: core.oam.dev/v1beta1 +kind: ComponentDefinition +metadata: + name: secretconsumer +spec: + workload: + definition: + apiVersion: apps/v1 + kind: Deployment + schematic: + cue: + template: | + output: { + apiVersion: "apps/v1" + kind: "Deployment" + spec: { + selector: matchLabels: { + "app.oam.dev/component": context.name + } + template: { + metadata: labels: { + "app.oam.dev/component": context.name + } + spec: { + containers: [{ + name: context.name + image: parameter.image + if parameter["dbSecret"] != _|_ { + env: [ + { + name: "username" + value: dbConn.username + }, + { + name: "DB_PASSWORD" + value: dbConn.password + }, + ] + } + }] + } + } + } + } + + parameter: { + // +usage=Which image would you like to use for your service + // +short=i + image: string + + // +usage=Referred db secret + // +insertSecretTo=dbConn + dbSecret?: string + } + + dbConn: { + username: string + password: string + } diff --git a/pkg/appfile/appfile.go b/pkg/appfile/appfile.go index 958248769..4453761f8 100644 --- a/pkg/appfile/appfile.go +++ b/pkg/appfile/appfile.go @@ -17,11 +17,14 @@ limitations under the License. package appfile import ( + "context" "encoding/json" "fmt" "regexp" "strings" + "github.com/oam-dev/kubevela/pkg/appfile/config" + "cuelang.org/go/cue" "cuelang.org/go/cue/format" json2cue "cuelang.org/go/encoding/json" @@ -70,6 +73,8 @@ type Workload struct { // RequiredSecrets stores secret names which the workload needs from cloud resource component and its context RequiredSecrets []process.RequiredSecrets UserConfigs []map[string]string + // ConfigNotReady indicates there's RequiredSecrets and UserConfigs but they're not ready yet. + ConfigNotReady bool } // GetUserConfigName get user config from AppFile, it will contain config file in it. @@ -103,15 +108,15 @@ func (wl *Workload) EvalHealth(ctx process.Context, client client.Client, namesp return wl.engine.HealthCheck(ctx, client, namespace, wl.FullTemplate.Health) } -// IsCloudResourceProducer checks whether a workload is cloud resource producer role -func (wl *Workload) IsCloudResourceProducer() bool { +// IsSecretProducer checks whether a workload is cloud resource producer role +func (wl *Workload) IsSecretProducer() bool { var existed bool _, existed = wl.Params[process.OutputSecretName] return existed } -// IsCloudResourceConsumer checks whether a workload is cloud resource consumer role -func (wl *Workload) IsCloudResourceConsumer() bool { +// IsSecretConsumer checks whether a workload is cloud resource consumer role +func (wl *Workload) IsSecretConsumer() bool { requiredSecretTag := strings.TrimRight(InsertSecretToTag, "=") matched, err := regexp.Match(regexp.QuoteMeta(requiredSecretTag), []byte(wl.FullTemplate.TemplateStr)) if err != nil || !matched { @@ -204,36 +209,35 @@ func generateUnstructuredFromCUEModule(wl *Workload, appName, revision, ns strin func (af *Appfile) GenerateComponentManifests() ([]*types.ComponentManifest, error) { compManifests := make([]*types.ComponentManifest, len(af.Workloads)) for i, wl := range af.Workloads { - switch wl.CapabilityCategory { - case types.HelmCategory: - cm, err := generateComponentFromHelmModule(wl, af.Name, af.RevisionName, af.Namespace) - if err != nil { - return nil, err - } - compManifests[i] = cm - case types.KubeCategory: - cm, err := generateComponentFromKubeModule(wl, af.Name, af.RevisionName, af.Namespace) - if err != nil { - return nil, err - } - compManifests[i] = cm - case types.TerraformCategory: - cm, err := generateComponentFromTerraformModule(wl, af.Name, af.RevisionName, af.Namespace) - if err != nil { - return nil, err - } - compManifests[i] = cm - default: - cm, err := generateComponentFromCUEModule(wl, af.Name, af.RevisionName, af.Namespace) - if err != nil { - return nil, err - } - compManifests[i] = cm + cm, err := af.GenerateComponentManifest(wl) + if err != nil { + return nil, err } + compManifests[i] = cm } return compManifests, nil } +// GenerateComponentManifest generate only one ComponentManifest +func (af *Appfile) GenerateComponentManifest(wl *Workload) (*types.ComponentManifest, error) { + if wl.ConfigNotReady { + return &types.ComponentManifest{ + Name: wl.Name, + InsertConfigNotReady: true, + }, nil + } + switch wl.CapabilityCategory { + case types.HelmCategory: + return generateComponentFromHelmModule(wl, af.Name, af.RevisionName, af.Namespace) + case types.KubeCategory: + return generateComponentFromKubeModule(wl, af.Name, af.RevisionName, af.Namespace) + case types.TerraformCategory: + return generateComponentFromTerraformModule(wl, af.Name, af.RevisionName, af.Namespace) + default: + return generateComponentFromCUEModule(wl, af.Name, af.RevisionName, af.Namespace) + } +} + // PrepareProcessContext prepares a DSL process Context func PrepareProcessContext(wl *Workload, applicationName, revision, namespace string) (process.Context, error) { pCtx := NewBasicContext(wl, applicationName, revision, namespace) @@ -253,6 +257,30 @@ func NewBasicContext(wl *Workload, applicationName, revision, namespace string) return pCtx } +// GetSecretAndConfigs will get secrets and configs the workload requires +func GetSecretAndConfigs(cli client.Client, workload *Workload, appName, ns string) error { + if workload.IsSecretConsumer() { + requiredSecrets, err := parseWorkloadInsertSecretTo(context.TODO(), cli, ns, workload) + if err != nil { + return err + } + workload.RequiredSecrets = requiredSecrets + } + + userConfig := workload.GetUserConfigName() + if userConfig != "" { + cg := config.Configmap{Client: cli} + // TODO(wonderflow): envName should not be namespace when we have serverside env + var envName = ns + data, err := cg.GetConfigData(config.GenConfigMapName(appName, workload.Name, userConfig), envName) + if err != nil { + return errors.Wrapf(err, "get config=%s for app=%s in namespace=%s", userConfig, appName, ns) + } + workload.UserConfigs = data + } + return nil +} + func generateComponentFromCUEModule(wl *Workload, appName, revision, ns string) (*types.ComponentManifest, error) { pCtx, err := PrepareProcessContext(wl, appName, revision, ns) if err != nil { @@ -271,7 +299,7 @@ func baseGenerateComponent(pCtx process.Context, wl *Workload, appName, ns strin outputSecretName string err error ) - if wl.IsCloudResourceProducer() { + if wl.IsSecretProducer() { outputSecretName, err = GetOutputSecretNames(wl) if err != nil { return nil, err diff --git a/pkg/appfile/parser.go b/pkg/appfile/parser.go index cb393edb2..960be54c5 100644 --- a/pkg/appfile/parser.go +++ b/pkg/appfile/parser.go @@ -21,6 +21,8 @@ import ( "fmt" "strings" + "k8s.io/klog/v2" + "cuelang.org/go/cue" "github.com/pkg/errors" v1 "k8s.io/api/core/v1" @@ -32,7 +34,6 @@ import ( "github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha2" "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" "github.com/oam-dev/kubevela/apis/types" - "github.com/oam-dev/kubevela/pkg/appfile/config" velacue "github.com/oam-dev/kubevela/pkg/cue" "github.com/oam-dev/kubevela/pkg/cue/definition" "github.com/oam-dev/kubevela/pkg/cue/packages" @@ -93,32 +94,38 @@ func (p *Parser) GenerateAppFile(ctx context.Context, app *v1beta1.Application) appfile.Namespace = ns var wds []*Workload for _, comp := range app.Spec.Components { - wd, err := p.parseWorkload(ctx, comp, appName, ns) + wd, err := p.parseWorkload(ctx, comp) if err != nil { return nil, err } + if err := GetSecretAndConfigs(p.client, wd, appName, ns); err != nil { + klog.InfoS("Failed to get secret and configs", "namespace", ns, "app name", appName, "workload name", wd.Name, + "err", err) + wd.ConfigNotReady = true + } + wds = append(wds, wd) } appfile.Workloads = wds var err error - appfile.Policies, err = p.parsePolicies(ctx, appName, ns, app.Spec.Policies) + appfile.Policies, err = p.parsePolicies(ctx, app.Spec.Policies) if err != nil { return nil, fmt.Errorf("failed to parsePolicies: %w", err) } - appfile.WorkflowSteps, err = p.parseWorkflow(ctx, appName, ns, app.Spec.Workflow) + appfile.WorkflowSteps, err = p.parseWorkflow(ctx, app.Spec.Workflow) if err != nil { return nil, fmt.Errorf("failed to parseWorkflow: %w", err) } return appfile, nil } -func (p *Parser) parsePolicies(ctx context.Context, appName, ns string, policies []v1beta1.AppPolicy) ([]*Workload, error) { +func (p *Parser) parsePolicies(ctx context.Context, policies []v1beta1.AppPolicy) ([]*Workload, error) { ws := []*Workload{} for _, policy := range policies { - w, err := p.makeWorkload(ctx, appName, ns, policy.Name, policy.Type, types.TypePolicy, policy.Properties) + w, err := p.makeWorkload(ctx, policy.Name, policy.Type, types.TypePolicy, policy.Properties) if err != nil { return nil, err } @@ -127,10 +134,10 @@ func (p *Parser) parsePolicies(ctx context.Context, appName, ns string, policies return ws, nil } -func (p *Parser) parseWorkflow(ctx context.Context, appName, ns string, steps []v1beta1.WorkflowStep) ([]*Workload, error) { +func (p *Parser) parseWorkflow(ctx context.Context, steps []v1beta1.WorkflowStep) ([]*Workload, error) { ws := []*Workload{} for _, step := range steps { - w, err := p.makeWorkload(ctx, appName, ns, step.Name, step.Type, types.TypeWorkflowStep, step.Properties) + w, err := p.makeWorkload(ctx, step.Name, step.Type, types.TypeWorkflowStep, step.Properties) if err != nil { return nil, err } @@ -139,7 +146,7 @@ func (p *Parser) parseWorkflow(ctx context.Context, appName, ns string, steps [] return ws, nil } -func (p *Parser) makeWorkload(ctx context.Context, appName, ns, name, typ string, capType types.CapType, props runtime.RawExtension) (*Workload, error) { +func (p *Parser) makeWorkload(ctx context.Context, name, typ string, capType types.CapType, props runtime.RawExtension) (*Workload, error) { templ, err := p.tmplLoader.LoadTemplate(ctx, p.dm, p.client, typ, capType) if err != nil && !kerrors.IsNotFound(err) { return nil, errors.WithMessagef(err, "fetch type of %s", name) @@ -162,33 +169,13 @@ func (p *Parser) makeWorkload(ctx context.Context, appName, ns, name, typ string Params: settings, engine: definition.NewWorkloadAbstractEngine(name, p.pd), } - - if workload.IsCloudResourceConsumer() { - requiredSecrets, err := parseWorkloadInsertSecretTo(ctx, p.client, ns, workload) - if err != nil { - return nil, err - } - workload.RequiredSecrets = requiredSecrets - } - - userConfig := workload.GetUserConfigName() - if userConfig != "" { - cg := config.Configmap{Client: p.client} - // TODO(wonderflow): envName should not be namespace when we have serverside env - var envName = ns - data, err := cg.GetConfigData(config.GenConfigMapName(appName, workload.Name, userConfig), envName) - if err != nil { - return nil, errors.Wrapf(err, "get config=%s for app=%s in namespace=%s", userConfig, appName, ns) - } - workload.UserConfigs = data - } return workload, nil } // parseWorkload resolve an ApplicationComponent and generate a Workload // containing ALL information required by an Appfile. -func (p *Parser) parseWorkload(ctx context.Context, comp v1beta1.ApplicationComponent, appName, ns string) (*Workload, error) { - workload, err := p.makeWorkload(ctx, appName, ns, comp.Name, comp.Type, types.TypeComponentDefinition, comp.Properties) +func (p *Parser) parseWorkload(ctx context.Context, comp v1beta1.ApplicationComponent) (*Workload, error) { + workload, err := p.makeWorkload(ctx, comp.Name, comp.Type, types.TypeComponentDefinition, comp.Properties) if err != nil { return nil, err } diff --git a/pkg/appfile/parser_test.go b/pkg/appfile/parser_test.go index c89df51bc..aa0006eca 100644 --- a/pkg/appfile/parser_test.go +++ b/pkg/appfile/parser_test.go @@ -616,7 +616,7 @@ parameter: { }) }) -var _ = Describe("Test IsCloudResourceProducer", func() { +var _ = Describe("Test IsSecretProducer", func() { Context("Workload is a Cloud Resource producer", func() { It("", func() { var targetSecretName = "db-conn" @@ -625,25 +625,25 @@ var _ = Describe("Test IsCloudResourceProducer", func() { "outputSecretName": targetSecretName, }, } - Expect(wl.IsCloudResourceProducer()).Should(Equal(true)) + Expect(wl.IsSecretProducer()).Should(Equal(true)) }) }) Context("Workload is a Cloud Resource producer", func() { It("", func() { wl := &Workload{} - Expect(wl.IsCloudResourceProducer()).Should(Equal(false)) + Expect(wl.IsSecretProducer()).Should(Equal(false)) }) }) }) -var _ = Describe("Test IsCloudResourceConsumer", func() { +var _ = Describe("Test IsSecretConsumer", func() { Context("Workload is a Cloud Resource consumer", func() { It("", func() { wl := &Workload{ FullTemplate: &Template{TemplateStr: "// +insertSecretTo=dbConn"}, } - Expect(wl.IsCloudResourceConsumer()).Should(Equal(true)) + Expect(wl.IsSecretConsumer()).Should(Equal(true)) }) }) @@ -652,7 +652,7 @@ var _ = Describe("Test IsCloudResourceConsumer", func() { wl := &Workload{ FullTemplate: &Template{TemplateStr: "// +useage=dbConn"}, } - Expect(wl.IsCloudResourceProducer()).Should(Equal(false)) + Expect(wl.IsSecretProducer()).Should(Equal(false)) }) }) }) diff --git a/pkg/appfile/validate.go b/pkg/appfile/validate.go index 01e74aa1d..cb0558398 100644 --- a/pkg/appfile/validate.go +++ b/pkg/appfile/validate.go @@ -33,6 +33,11 @@ func (p *Parser) ValidateCUESchematicAppfile(a *Appfile) error { if wl.CapabilityCategory != types.CUECategory { continue } + if wl.IsSecretConsumer() { + // we don't check CUE schema when it's has secret insert demand as the secret can not be ready + // we should deprecate the secret consumer when workflow ready and add the check back + continue + } pCtx, err := newValidationProcessContext(wl, a.Name, a.RevisionName, a.Namespace) if err != nil { @@ -60,10 +65,6 @@ func newValidationProcessContext(wl *Workload, appName, revisionName, ns string) } pCtx := process.NewContextWithHooks(ns, wl.Name, appName, revisionName, baseHooks, auxiliaryHooks) - pCtx.InsertSecrets(wl.OutputSecretName, wl.RequiredSecrets) - if len(wl.UserConfigs) > 0 { - pCtx.SetConfigs(wl.UserConfigs) - } if err := wl.EvalContext(pCtx); err != nil { return nil, errors.Wrapf(err, "evaluate base template app=%s in namespace=%s", appName, ns) } diff --git a/pkg/controller/core.oam.dev/v1alpha2/application/application_controller.go b/pkg/controller/core.oam.dev/v1alpha2/application/application_controller.go index 398a7065d..5f63ec0b8 100644 --- a/pkg/controller/core.oam.dev/v1alpha2/application/application_controller.go +++ b/pkg/controller/core.oam.dev/v1alpha2/application/application_controller.go @@ -133,30 +133,20 @@ func (r *Reconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { "revisionHash", handler.currentRevHash, "isNewRevision", handler.isNewRevision) var comps []*velatypes.ComponentManifest - if handler.isNewRevision { - comps, err = appFile.GenerateComponentManifests() - if err != nil { - klog.ErrorS(err, "Failed to render components", "application", klog.KObj(app)) - app.Status.SetConditions(errorCondition("Render", err)) - r.Recorder.Event(app, event.Warning(velatypes.ReasonFailedRender, err)) - return handler.handleErr(err) - } - if err := handler.handleComponentsRevision(ctx, comps); err != nil { - klog.ErrorS(err, "Failed to handle compoents revision", "application", klog.KObj(app)) - app.Status.SetConditions(errorCondition("Render", err)) - r.Recorder.Event(app, event.Warning(velatypes.ReasonFailedRevision, err)) - return handler.handleErr(err) - } - } else { - comps, err = oamutil.AppConfig2ComponentManifests(handler.latestAppRev.Spec.ApplicationConfiguration, - handler.latestAppRev.Spec.Components) - if err != nil { - klog.ErrorS(err, "Failed to get data from existing app revision", "application", klog.KObj(app)) - app.Status.SetConditions(errorCondition("Revision", err)) - r.Recorder.Event(app, event.Warning(velatypes.ReasonFailedRevision, err)) - return handler.handleErr(err) - } + comps, err = appFile.GenerateComponentManifests() + if err != nil { + klog.ErrorS(err, "Failed to render components", "application", klog.KObj(app)) + app.Status.SetConditions(errorCondition("Render", err)) + r.Recorder.Event(app, event.Warning(velatypes.ReasonFailedRender, err)) + return handler.handleErr(err) } + if err := handler.handleComponentsRevision(ctx, comps); err != nil { + klog.ErrorS(err, "Failed to handle compoents revision", "application", klog.KObj(app)) + app.Status.SetConditions(errorCondition("Render", err)) + r.Recorder.Event(app, event.Warning(velatypes.ReasonFailedRevision, err)) + return handler.handleErr(err) + } + if err := handler.finalizeAndApplyAppRevision(ctx, comps); err != nil { klog.ErrorS(err, "Failed to apply app revision", "application", klog.KObj(app)) app.Status.SetConditions(errorCondition("Revision", err)) diff --git a/pkg/controller/core.oam.dev/v1alpha2/application/application_controller_test.go b/pkg/controller/core.oam.dev/v1alpha2/application/application_controller_test.go index cef291c89..a3c84e82f 100644 --- a/pkg/controller/core.oam.dev/v1alpha2/application/application_controller_test.go +++ b/pkg/controller/core.oam.dev/v1alpha2/application/application_controller_test.go @@ -1505,8 +1505,84 @@ spec: By("Delete Application, clean the resource") Expect(k8sClient.Delete(ctx, app)).Should(BeNil()) }) + + It("app with two components and one component can apply first while another one has secret insert", func() { + appMix := appWithTwoComp.DeepCopy() + appMix.Spec.Components[1] = v1beta1.ApplicationComponent{ + Name: "myconsumer", + Type: "secretconsumer", + Properties: runtime.RawExtension{Raw: []byte(`{"image":"nginx:1.14.0", "dbSecret":"mys"}`)}, + } + ns := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "vela-test-two-components-insert-secrets", + }, + } + appMix.SetName("vela-test-two-components-insert-secrets") + appMix.SetNamespace(ns.Name) + + secretconsumer := &v1beta1.ComponentDefinition{} + wDDefJson, _ := yaml.YAMLToJSON([]byte(compDefSecretYaml)) + Expect(json.Unmarshal(wDDefJson, secretconsumer)).Should(BeNil()) + secretconsumer.SetNamespace(ns.Name) + Expect(k8sClient.Create(ctx, ns)).Should(BeNil()) + Expect(k8sClient.Create(ctx, secretconsumer)).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{})) + Expect(k8sClient.Create(ctx, appMix.DeepCopyObject())).Should(BeNil()) + + appKey := client.ObjectKey{ + Name: appMix.Name, + Namespace: appMix.Namespace, + } + res, _ := reconcileOnceAfterFinalizer(reconciler, reconcile.Request{NamespacedName: appKey}) + Expect(res.RequeueAfter).ShouldNot(BeEquivalentTo(0)) + + By("Check Application Created with the correct phase") + curApp := &v1beta1.Application{} + Expect(k8sClient.Get(ctx, appKey, curApp)).Should(BeNil()) + Expect(curApp.Status.Phase).Should(Equal(common.ApplicationHealthChecking)) + + By("Check One of the component created as expected") + comp1 := &v1.Deployment{} + Expect(k8sClient.Get(ctx, client.ObjectKey{ + Namespace: curApp.Namespace, + Name: appMix.Spec.Components[0].Name, + }, comp1)).Should(BeNil()) + + By("Check another component not existed") + comp2 := &v1.Deployment{} + Expect(k8sClient.Get(ctx, client.ObjectKey{ + Namespace: curApp.Namespace, + Name: appMix.Spec.Components[1].Name, + }, comp2)).ShouldNot(BeNil()) + sec := &corev1.Secret{Data: map[string][]byte{ + "username": []byte("abc"), + "password": []byte("123"), + }} + sec.Name = "mys" + sec.Namespace = appMix.Namespace + Expect(k8sClient.Create(ctx, sec)).Should(BeNil()) + + reconcileRetry(reconciler, reconcile.Request{NamespacedName: appKey}) + + By("Check another component is existed") + comp2 = &v1.Deployment{} + Expect(k8sClient.Get(ctx, client.ObjectKey{ + Namespace: curApp.Namespace, + Name: appMix.Spec.Components[1].Name, + }, comp2)).Should(BeNil()) + Expect(comp2.Spec.Template.Spec.Containers[0].Env[0].Value).Should(BeEquivalentTo("abc")) + Expect(comp2.Spec.Template.Spec.Containers[0].Env[1].Value).Should(BeEquivalentTo("123")) + }) }) +func reconcileOnceAfterFinalizer(r reconcile.Reconciler, req reconcile.Request) (reconcile.Result, error) { + // 1st and 2nd time reconcile to add finalizer + r.Reconcile(req) + r.Reconcile(req) + + return r.Reconcile(req) +} + func reconcileRetry(r reconcile.Reconciler, req reconcile.Request) { // 1st and 2nd time reconcile to add finalizer Eventually(func() error { @@ -2277,6 +2353,67 @@ spec: port: string } ` + + compDefSecretYaml = `apiVersion: core.oam.dev/v1beta1 +kind: ComponentDefinition +metadata: + name: secretconsumer +spec: + workload: + definition: + apiVersion: apps/v1 + kind: Deployment + schematic: + cue: + template: | + output: { + apiVersion: "apps/v1" + kind: "Deployment" + spec: { + selector: matchLabels: { + "app.oam.dev/component": context.name + } + template: { + metadata: labels: { + "app.oam.dev/component": context.name + } + spec: { + containers: [{ + name: context.name + image: parameter.image + if parameter["dbSecret"] != _|_ { + env: [ + { + name: "username" + value: dbConn.username + }, + { + name: "DB_PASSWORD" + value: dbConn.password + }, + ] + } + }] + } + } + } + } + + parameter: { + // +usage=Which image would you like to use for your service + // +short=i + image: string + + // +usage=Referred db secret + // +insertSecretTo=dbConn + dbSecret?: string + } + + dbConn: { + username: string + password: string + } +` ) func newMockHTTP() *httptest.Server { diff --git a/pkg/controller/core.oam.dev/v1alpha2/application/apply.go b/pkg/controller/core.oam.dev/v1alpha2/application/apply.go index 17306fdb7..2a988b894 100644 --- a/pkg/controller/core.oam.dev/v1alpha2/application/apply.go +++ b/pkg/controller/core.oam.dev/v1alpha2/application/apply.go @@ -18,7 +18,6 @@ package application import ( "context" - "fmt" "time" runtimev1alpha1 "github.com/crossplane/crossplane-runtime/apis/core/v1alpha1" @@ -93,8 +92,8 @@ func (h *appHandler) applyAppManifests(ctx context.Context, comps []*types.Compo return errors.WithMessage(err, "cannot dispatch packaged workload resources") } } - if checkAutoDetectComponent(comp.StandardWorkload) { - return fmt.Errorf("helm mode component doesn't specify workload, the traits attached to the helm mode component will fail to work") + if comp.InsertConfigNotReady { + continue } } a := assemble.NewAppManifests(appRev).WithWorkloadOption(assemble.DiscoveryHelmBasedWorkload(ctx, h.r.Client)) @@ -108,13 +107,6 @@ func (h *appHandler) applyAppManifests(ctx context.Context, comps []*types.Compo return nil } -// checkAutoDetectComponent will check if the standardWorkload is empty, -// currently only Helm-based component is possible to be auto-detected -// TODO implement auto-detect mechanism -func checkAutoDetectComponent(wl *unstructured.Unstructured) bool { - return wl == nil || (len(wl.GetAPIVersion()) == 0 && len(wl.GetKind()) == 0) -} - func (h *appHandler) aggregateHealthStatus(appFile *appfile.Appfile) ([]common.ApplicationComponentStatus, bool, error) { var appStatus []common.ApplicationComponentStatus var healthy = true @@ -131,7 +123,15 @@ func (h *appHandler) aggregateHealthStatus(appFile *appfile.Appfile) ([]common.A pCtx process.Context ) - if wl.IsCloudResourceProducer() { + // this can help detect the componentManifest not ready and reconcile again + if wl.ConfigNotReady { + status.Healthy = false + status.Message = "secrets or configs not ready" + appStatus = append(appStatus, status) + healthy = false + continue + } + if wl.IsSecretProducer() { outputSecretName, err = appfile.GetOutputSecretNames(wl) if err != nil { return nil, false, errors.WithMessagef(err, "app=%s, comp=%s, setting outputSecretName error", appFile.Name, wl.Name) diff --git a/pkg/controller/core.oam.dev/v1alpha2/application/assemble/assemble.go b/pkg/controller/core.oam.dev/v1alpha2/application/assemble/assemble.go index a2334c706..28e39d3a3 100644 --- a/pkg/controller/core.oam.dev/v1alpha2/application/assemble/assemble.go +++ b/pkg/controller/core.oam.dev/v1alpha2/application/assemble/assemble.go @@ -164,6 +164,13 @@ func (am *AppManifests) GroupAssembledManifests() ( return workloads, traits, scopes, nil } +// checkAutoDetectComponent will check if the standardWorkload is empty, +// currently only Helm-based component is possible to be auto-detected +// TODO implement auto-detect mechanism +func checkAutoDetectComponent(wl *unstructured.Unstructured) bool { + return wl == nil || (len(wl.GetAPIVersion()) == 0 && len(wl.GetKind()) == 0) +} + func (am *AppManifests) assemble() { am.complete() klog.InfoS("Assemble manifests for application", "name", am.appName, "revision", am.AppRevision.GetName()) @@ -172,6 +179,13 @@ func (am *AppManifests) assemble() { return } for _, comp := range am.componentManifests { + if comp.InsertConfigNotReady { + continue + } + if checkAutoDetectComponent(comp.StandardWorkload) { + klog.Warningf("component without specify workloadDef can not attach traits currently") + continue + } compRevisionName := comp.RevisionName compName := comp.Name commonLabels := am.generateAndFilterCommonLabels(compName, compRevisionName) diff --git a/pkg/controller/core.oam.dev/v1alpha2/application/revision.go b/pkg/controller/core.oam.dev/v1alpha2/application/revision.go index 3823df0c9..603697e32 100644 --- a/pkg/controller/core.oam.dev/v1alpha2/application/revision.go +++ b/pkg/controller/core.oam.dev/v1alpha2/application/revision.go @@ -59,12 +59,18 @@ func (h *appHandler) createResourcesConfigMap(ctx context.Context, buf := &bytes.Buffer{} for _, c := range comps { + if c.InsertConfigNotReady { + continue + } r := c.StandardWorkload.DeepCopy() r.SetName(c.Name) r.SetNamespace(appRev.Namespace) buf.Write(util.MustJSONMarshal(r)) } for _, c := range comps { + if c.InsertConfigNotReady { + continue + } for _, tr := range c.Traits { r := tr.DeepCopy() r.SetName(c.Name) @@ -302,6 +308,9 @@ func DeepEqualRevision(old, new *v1beta1.ApplicationRevision) bool { func (h *appHandler) handleComponentsRevision(ctx context.Context, compManifests []*types.ComponentManifest) error { for _, cm := range compManifests { + if cm.InsertConfigNotReady { + continue + } hash, err := computeComponentRevisionHash(cm) if err != nil { return err @@ -471,6 +480,19 @@ func componentManifests2AppConfig(cms []*types.ComponentManifest) (runtime.RawEx for i, cm := range cms { acc := v1alpha2.ApplicationConfigurationComponent{} acc.ComponentName = cm.Name + comp := &v1alpha2.Component{} + comp.SetGroupVersionKind(v1alpha2.ComponentGroupVersionKind) + comp.SetName(cm.Name) + + if cm.InsertConfigNotReady { + // -- represent the component is not ready at all + acc.RevisionName = "--" + + comps[i] = common.RawComponent{Raw: util.Object2RawExtension(comp)} + ac.Spec.Components[i] = acc + continue + } + acc.RevisionName = cm.RevisionName acc.Traits = make([]v1alpha2.ComponentTrait, len(cm.Traits)) for j, t := range cm.Traits { @@ -488,9 +510,7 @@ func componentManifests2AppConfig(cms []*types.ComponentManifest) (runtime.RawEx }, } } - comp := &v1alpha2.Component{} - comp.SetGroupVersionKind(v1alpha2.ComponentGroupVersionKind) - comp.SetName(cm.Name) + // this label is very important for handling component revision util.AddLabels(comp, map[string]string{ oam.LabelComponentRevisionHash: cm.RevisionHash, diff --git a/pkg/oam/util/helper.go b/pkg/oam/util/helper.go index 1370f9286..df20f80d3 100644 --- a/pkg/oam/util/helper.go +++ b/pkg/oam/util/helper.go @@ -660,6 +660,11 @@ func AppConfig2ComponentManifests(acRaw runtime.RawExtension, comps []common.Raw Name: acc.ComponentName, RevisionName: acc.RevisionName, } + if acc.RevisionName == "--" { + // generate ComponentManifest from appfile + cms[i] = cm + continue + } if acc.ComponentName == "" && acc.RevisionName != "" { cm.Name = ExtractComponentName(acc.RevisionName) } diff --git a/references/appfile/dryrun/testdata/dryrun-exp-comp.yaml b/references/appfile/dryrun/testdata/dryrun-exp-comp.yaml index f28e0a11b..416de929f 100644 --- a/references/appfile/dryrun/testdata/dryrun-exp-comp.yaml +++ b/references/appfile/dryrun/testdata/dryrun-exp-comp.yaml @@ -1,3 +1,4 @@ +InsertConfigNotReady: false Name: myweb PackagedTraitResources: null PackagedWorkloadResources: null