mirror of
https://github.com/kubevela/kubevela.git
synced 2026-05-07 18:07:30 +00:00
1011 lines
37 KiB
Go
1011 lines
37 KiB
Go
/*
|
|
Copyright 2021 The Crossplane 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 applicationconfiguration
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"reflect"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/crossplane/crossplane-runtime/pkg/fieldpath"
|
|
"github.com/crossplane/crossplane-runtime/pkg/resource"
|
|
"github.com/pkg/errors"
|
|
corev1 "k8s.io/api/core/v1"
|
|
kerrors "k8s.io/apimachinery/pkg/api/errors"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
"k8s.io/apimachinery/pkg/types"
|
|
"k8s.io/apimachinery/pkg/util/intstr"
|
|
"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"
|
|
oamtype "github.com/oam-dev/kubevela/apis/types"
|
|
helmapi "github.com/oam-dev/kubevela/pkg/appfile/helm/flux2apis"
|
|
"github.com/oam-dev/kubevela/pkg/controller/common"
|
|
"github.com/oam-dev/kubevela/pkg/controller/utils"
|
|
"github.com/oam-dev/kubevela/pkg/oam"
|
|
"github.com/oam-dev/kubevela/pkg/oam/discoverymapper"
|
|
"github.com/oam-dev/kubevela/pkg/oam/util"
|
|
)
|
|
|
|
// Render error strings.
|
|
const (
|
|
errUnmarshalWorkload = "cannot unmarshal workload"
|
|
errUnmarshalTrait = "cannot unmarshal trait"
|
|
)
|
|
|
|
// Render error format strings.
|
|
const (
|
|
errFmtGetComponent = "cannot get component %q"
|
|
errFmtGetScope = "cannot get scope %q"
|
|
errFmtResolveParams = "cannot resolve parameter values for component %q"
|
|
errFmtRenderWorkload = "cannot render workload for component %q"
|
|
errFmtRenderTrait = "cannot render trait for component %q"
|
|
errFmtSetParam = "cannot set parameter %q"
|
|
errFmtUnsupportedParam = "unsupported parameter %q"
|
|
errFmtRequiredParam = "required parameter %q not specified"
|
|
errFmtCompRevision = "cannot get latest revision for component %q while revision is enabled"
|
|
)
|
|
|
|
var (
|
|
// ErrDataOutputNotExist is an error indicating the DataOutput specified doesn't not exist
|
|
ErrDataOutputNotExist = errors.New("DataOutput does not exist")
|
|
)
|
|
|
|
// A ComponentRenderer renders an ApplicationConfiguration's Components into
|
|
// workloads and traits.
|
|
type ComponentRenderer interface {
|
|
Render(ctx context.Context, ac *v1alpha2.ApplicationConfiguration) ([]Workload, *v1alpha2.DependencyStatus, error)
|
|
}
|
|
|
|
// A ComponentRenderFn renders an ApplicationConfiguration's Components into
|
|
// workloads and traits.
|
|
type ComponentRenderFn func(ctx context.Context, ac *v1alpha2.ApplicationConfiguration) ([]Workload, *v1alpha2.DependencyStatus, error)
|
|
|
|
// Render an ApplicationConfiguration's Components into workloads and traits.
|
|
func (fn ComponentRenderFn) Render(ctx context.Context, ac *v1alpha2.ApplicationConfiguration) ([]Workload, *v1alpha2.DependencyStatus, error) {
|
|
return fn(ctx, ac)
|
|
}
|
|
|
|
var _ ComponentRenderer = &components{}
|
|
|
|
type components struct {
|
|
// indicate that if this is generated by application
|
|
client client.Reader
|
|
dm discoverymapper.DiscoveryMapper
|
|
params ParameterResolver
|
|
workload ResourceRenderer
|
|
trait ResourceRenderer
|
|
}
|
|
|
|
func (r *components) Render(ctx context.Context, ac *v1alpha2.ApplicationConfiguration) ([]Workload, *v1alpha2.DependencyStatus, error) {
|
|
workloads := make([]*Workload, 0, len(ac.Spec.Components))
|
|
dag := newDAG()
|
|
|
|
// we have special logics for application generated applicationConfiguration during rolling out phase
|
|
rollingComponents := make(map[string]bool)
|
|
var needRolloutTemplate bool
|
|
if _, isAppRolling := ac.GetAnnotations()[oam.AnnotationAppRollout]; isAppRolling {
|
|
// we only care about the new components when there is rolling out
|
|
if anc, exist := ac.GetAnnotations()[oam.AnnotationRollingComponent]; exist {
|
|
// the rolling components annotation contains all the rolling components in the application
|
|
for _, componentName := range strings.Split(anc, common.RollingComponentsSep) {
|
|
rollingComponents[componentName] = true
|
|
}
|
|
}
|
|
// we need to do a template roll out if it's not done yet or forced
|
|
needRolloutTemplate = ac.Status.RollingStatus != oamtype.RollingTemplated
|
|
if needRolloutTemplate {
|
|
klog.InfoS("need to template the ac ", "appConfig", klog.KRef(ac.Namespace, ac.Name),
|
|
"rolling status", ac.Status.RollingStatus)
|
|
}
|
|
} else if ac.Status.RollingStatus == oamtype.RollingTemplated {
|
|
klog.InfoS("mark the ac rolling status as completed", "appConfig", klog.KRef(ac.Namespace, ac.Name))
|
|
ac.Status.RollingStatus = oamtype.RollingCompleted
|
|
}
|
|
|
|
for _, acc := range ac.Spec.Components {
|
|
if acc.RevisionName != "" {
|
|
acc.ComponentName = utils.ExtractComponentName(acc.RevisionName)
|
|
}
|
|
isComponentRolling := rollingComponents[acc.ComponentName]
|
|
w, err := r.renderComponent(ctx, acc, ac, isControlledByApp(ac), isComponentRolling, needRolloutTemplate, dag)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
workloads = append(workloads, w)
|
|
// TODO: handle rolling status better when there are multiple components
|
|
if isComponentRolling && needRolloutTemplate {
|
|
ac.Status.RollingStatus = oamtype.RollingTemplating
|
|
}
|
|
}
|
|
ds := &v1alpha2.DependencyStatus{}
|
|
res := make([]Workload, 0, len(ac.Spec.Components))
|
|
for i, acc := range ac.Spec.Components {
|
|
unsatisfied, err := r.handleDependency(ctx, workloads[i], acc, dag, ac)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
ds.Unsatisfied = append(ds.Unsatisfied, unsatisfied...)
|
|
res = append(res, *workloads[i])
|
|
}
|
|
|
|
return res, ds, nil
|
|
}
|
|
|
|
func (r *components) renderComponent(ctx context.Context, acc v1alpha2.ApplicationConfigurationComponent,
|
|
ac *v1alpha2.ApplicationConfiguration, isControlledByApp, isComponentRolling, needRolloutTemplate bool, dag *dag) (*Workload, error) {
|
|
if acc.RevisionName != "" {
|
|
acc.ComponentName = utils.ExtractComponentName(acc.RevisionName)
|
|
}
|
|
klog.InfoS("render a component", "component name", acc.ComponentName, "component revision", acc.RevisionName,
|
|
"is generated by application", isControlledByApp, "is the component rolling", isComponentRolling,
|
|
"is the appConfig a rollout template", needRolloutTemplate)
|
|
|
|
c, componentRevisionName, err := util.GetComponent(ctx, r.client, acc, ac.GetNamespace())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
p, err := r.params.Resolve(c.Spec.Parameters, acc.ParameterValues)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, errFmtResolveParams, acc.ComponentName)
|
|
}
|
|
|
|
w, err := r.workload.Render(c.Spec.Workload.Raw, p...)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, errFmtRenderWorkload, acc.ComponentName)
|
|
}
|
|
|
|
compInfoLabels := map[string]string{
|
|
oam.LabelAppName: ac.Name,
|
|
oam.LabelAppComponent: acc.ComponentName,
|
|
oam.LabelAppComponentRevision: componentRevisionName,
|
|
oam.LabelOAMResourceType: oam.ResourceTypeWorkload,
|
|
}
|
|
util.AddLabels(w, compInfoLabels)
|
|
|
|
compInfoAnnotations := map[string]string{
|
|
oam.AnnotationAppGeneration: strconv.Itoa(int(ac.Generation)),
|
|
}
|
|
util.AddAnnotations(w, compInfoAnnotations)
|
|
var inplaceUpgrade string
|
|
if acAnnotations := ac.GetAnnotations(); acAnnotations != nil {
|
|
inplaceUpgrade = acAnnotations[oam.AnnotationInplaceUpgrade]
|
|
}
|
|
// pass through labels and annotation from app-config to workload
|
|
util.PassLabelAndAnnotation(ac, w)
|
|
// don't pass the following annotation as those are for appConfig only
|
|
util.RemoveAnnotations(w, []string{oam.AnnotationAppRollout, oam.AnnotationRollingComponent, oam.AnnotationInplaceUpgrade})
|
|
ref := getOwnerFromAC(ac)
|
|
|
|
// Don't override if the resources already has namespace, it was set by user or the application controller which is by design.
|
|
if len(w.GetNamespace()) == 0 {
|
|
w.SetNamespace(ac.GetNamespace())
|
|
}
|
|
traits := make([]*Trait, 0, len(acc.Traits))
|
|
traitDefs := make([]v1alpha2.TraitDefinition, 0, len(acc.Traits))
|
|
compInfoLabels[oam.LabelOAMResourceType] = oam.ResourceTypeTrait
|
|
|
|
for _, ct := range acc.Traits {
|
|
t, traitDef, err := r.renderTrait(ctx, ct, ac, acc.ComponentName, ref, dag)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
util.AddLabels(t, compInfoLabels)
|
|
util.AddAnnotations(t, compInfoAnnotations)
|
|
|
|
// pass through labels and annotation from app-config to trait
|
|
util.PassLabelAndAnnotation(ac, t)
|
|
util.RemoveAnnotations(t, []string{oam.AnnotationAppRollout, oam.AnnotationRollingComponent, oam.AnnotationInplaceUpgrade})
|
|
traits = append(traits, &Trait{Object: *t, Definition: *traitDef})
|
|
traitDefs = append(traitDefs, *traitDef)
|
|
}
|
|
if !isControlledByApp {
|
|
// This is the legacy standalone appConfig approach
|
|
existingWorkload, err := r.getExistingWorkload(ctx, ac, c, w)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := setWorkloadInstanceName(traitDefs, w, c, existingWorkload); err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
// we have completely different approaches on workload name for application generated appConfig
|
|
if c.Spec.Helm != nil {
|
|
// for helm workload, make sure the workload is already generated by Helm successfully
|
|
existingWorkloadByHelm, err := discoverHelmModuleWorkload(ctx, r.client, c, ac.GetNamespace())
|
|
if err != nil {
|
|
klog.ErrorS(err, "Could not get the workload created by Helm module",
|
|
"component name", acc.ComponentName, "component revision", acc.RevisionName)
|
|
return nil, errors.Wrap(err, "cannot get the workload created by a Helm module")
|
|
}
|
|
klog.InfoS("Successfully discovered the workload created by Helm",
|
|
"component name", acc.ComponentName, "component revision", acc.RevisionName,
|
|
"workload name", existingWorkloadByHelm.GetName())
|
|
// use the name already generated instead of setting a new one
|
|
w.SetName(existingWorkloadByHelm.GetName())
|
|
} else {
|
|
// for non-helm workload, we generate a workload name based on component name and revision
|
|
revision, err := utils.ExtractRevision(acc.RevisionName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// Pass inpalce upgrade into it
|
|
SetAppWorkloadInstanceName(acc.ComponentName, w, revision, inplaceUpgrade)
|
|
if isComponentRolling && needRolloutTemplate {
|
|
// we have a special logic to emit the workload as a template so that the rollout
|
|
// controller can take over.
|
|
// TODO: We might need to add the owner reference to the existing object in case the resource
|
|
// is going to be shared (ie. CloneSet)
|
|
if err := prepWorkloadInstanceForRollout(w); err != nil {
|
|
return nil, err
|
|
}
|
|
// yield the controller to the rollout
|
|
ref.Controller = pointer.BoolPtr(false)
|
|
klog.InfoS("Successfully rendered a workload instance for rollout", "workload", w.GetName())
|
|
}
|
|
}
|
|
}
|
|
// set the owner reference after its ref is edited
|
|
// If workload is in different namespace with application set the ownerReference, otherwise the owner was set with a resourceTracker by application controller already.
|
|
if ac.GetNamespace() == w.GetNamespace() {
|
|
w.SetOwnerReferences([]metav1.OwnerReference{*ref})
|
|
}
|
|
|
|
// create the ref after the workload name is set
|
|
workloadRef := corev1.ObjectReference{
|
|
APIVersion: w.GetAPIVersion(),
|
|
Kind: w.GetKind(),
|
|
Name: w.GetName(),
|
|
}
|
|
// We only patch a TypedReference object to the trait if it asks for it
|
|
for i := range acc.Traits {
|
|
traitDef := traitDefs[i]
|
|
trait := traits[i]
|
|
workloadRefPath := traitDef.Spec.WorkloadRefPath
|
|
if len(workloadRefPath) != 0 {
|
|
if err := fieldpath.Pave(trait.Object.UnstructuredContent()).SetValue(workloadRefPath, workloadRef); err != nil {
|
|
return nil, errors.Wrapf(err, errFmtSetWorkloadRef, trait.Object.GetName(), w.GetName())
|
|
}
|
|
}
|
|
}
|
|
scopes := make([]unstructured.Unstructured, 0, len(acc.Scopes))
|
|
for _, cs := range acc.Scopes {
|
|
scopeObject, err := r.renderScope(ctx, cs, ac.GetNamespace())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
scopes = append(scopes, *scopeObject)
|
|
}
|
|
|
|
addDataOutputsToDAG(dag, acc.DataOutputs, w)
|
|
// To avoid conflict with rollout controller, we will not render the workload until the rollout phase is over
|
|
// indicated by the AnnotationAppRollout annotation disappear
|
|
return &Workload{ComponentName: acc.ComponentName, ComponentRevisionName: componentRevisionName,
|
|
SkipApply: isComponentRolling && !needRolloutTemplate,
|
|
Workload: w, Traits: traits, RevisionEnabled: isRevisionEnabled(traitDefs), Scopes: scopes}, nil
|
|
}
|
|
|
|
func (r *components) renderTrait(ctx context.Context, ct v1alpha2.ComponentTrait, ac *v1alpha2.ApplicationConfiguration,
|
|
componentName string, ref *metav1.OwnerReference, dag *dag) (*unstructured.Unstructured, *v1alpha2.TraitDefinition, error) {
|
|
t, err := r.trait.Render(ct.Trait.Raw)
|
|
if err != nil {
|
|
return nil, nil, errors.Wrapf(err, errFmtRenderTrait, componentName)
|
|
}
|
|
traitDef, err := util.FetchTraitDefinition(ctx, r.client, r.dm, t)
|
|
if err != nil {
|
|
if !kerrors.IsNotFound(err) {
|
|
return nil, nil, errors.Wrapf(err, errFmtGetTraitDefinition, t.GetAPIVersion(), t.GetKind(), t.GetName())
|
|
}
|
|
traitDef = util.GetDummyTraitDefinition(t)
|
|
}
|
|
|
|
traitName := getTraitName(ac, componentName, &ct, t, traitDef)
|
|
setTraitProperties(t, traitName, ac.GetNamespace(), ref)
|
|
|
|
addDataOutputsToDAG(dag, ct.DataOutputs, t)
|
|
|
|
return t, traitDef, nil
|
|
}
|
|
|
|
func (r *components) renderScope(ctx context.Context, cs v1alpha2.ComponentScope, ns string) (*unstructured.Unstructured, error) {
|
|
// Get Scope instance from k8s, since it is global and not a child resource of workflow.
|
|
scopeObject := &unstructured.Unstructured{}
|
|
scopeObject.SetAPIVersion(cs.ScopeReference.APIVersion)
|
|
scopeObject.SetKind(cs.ScopeReference.Kind)
|
|
scopeObjectRef := types.NamespacedName{Namespace: ns, Name: cs.ScopeReference.Name}
|
|
if err := r.client.Get(ctx, scopeObjectRef, scopeObject); err != nil {
|
|
return nil, errors.Wrapf(err, errFmtGetScope, cs.ScopeReference.Name)
|
|
}
|
|
return scopeObject, nil
|
|
}
|
|
|
|
func setTraitProperties(t *unstructured.Unstructured, traitName, namespace string, ref *metav1.OwnerReference) {
|
|
// Set metadata name for `Trait` if the metadata name is NOT set.
|
|
if t.GetName() == "" {
|
|
t.SetName(traitName)
|
|
}
|
|
|
|
if controller := metav1.GetControllerOf(t); controller != nil {
|
|
if controller.APIVersion == v1beta1.SchemeGroupVersion.String() &&
|
|
controller.Kind == v1beta1.ResourceTrackerKind {
|
|
// if a resource is controlled by a ResourceTracker,
|
|
// it's cluster-scoped or in the different namespace with
|
|
// application, so no need to check/set namespace
|
|
return
|
|
}
|
|
}
|
|
// Don't override if the resources already has namespace, it was set by user or the application controller which is by design.
|
|
if len(t.GetNamespace()) == 0 {
|
|
t.SetNamespace(namespace)
|
|
}
|
|
// If trait is in different namespace with application set the ownerReference, otherwise the owner was set with a resourceTracker by application controller already.
|
|
if t.GetNamespace() == namespace {
|
|
t.SetOwnerReferences([]metav1.OwnerReference{*ref})
|
|
}
|
|
|
|
}
|
|
|
|
// setWorkloadInstanceName will set metadata.name for workload CR according to createRevision flag in traitDefinition
|
|
func setWorkloadInstanceName(traitDefs []v1alpha2.TraitDefinition, w *unstructured.Unstructured,
|
|
c *v1alpha2.Component, existingWorkload *unstructured.Unstructured) error {
|
|
// Don't override the specified name
|
|
if w.GetName() != "" {
|
|
return nil
|
|
}
|
|
// TODO: revisit this logic
|
|
// the name of the workload should depend on the workload type and if we are rolling or replacing upgrade
|
|
// i.e Cloneset type of workload just use the component name while deployment type of workload will have revision
|
|
// if we are doing rolling upgrades. We can just override if we are replacing the deployment.
|
|
if isRevisionEnabled(traitDefs) {
|
|
if c.Status.LatestRevision == nil {
|
|
return fmt.Errorf(errFmtCompRevision, c.Name)
|
|
}
|
|
|
|
componentLastRevision := c.Status.LatestRevision.Name
|
|
// if workload exists, check the revision label, we will not change the name if workload exists and no revision changed
|
|
if existingWorkload != nil && existingWorkload.GetLabels()[oam.LabelAppComponentRevision] == componentLastRevision {
|
|
// using the existing name
|
|
w.SetName(existingWorkload.GetName())
|
|
return nil
|
|
}
|
|
|
|
// if revisionEnabled and the running workload's revision isn't equal to the component's latest reversion,
|
|
// use revisionName as the workload name
|
|
w.SetName(componentLastRevision)
|
|
return nil
|
|
}
|
|
// use component name as workload name, which means we will always use one workload for different revisions
|
|
w.SetName(c.GetName())
|
|
return nil
|
|
}
|
|
|
|
// isRevisionEnabled will check if any of the traitDefinitions has a createRevision flag
|
|
func isRevisionEnabled(traitDefs []v1alpha2.TraitDefinition) bool {
|
|
for _, td := range traitDefs {
|
|
if td.Spec.RevisionEnabled {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// A ResourceRenderer renders a Kubernetes-compliant YAML resource into an
|
|
// Unstructured object, optionally setting the supplied parameters.
|
|
type ResourceRenderer interface {
|
|
Render(data []byte, p ...Parameter) (*unstructured.Unstructured, error)
|
|
}
|
|
|
|
// A ResourceRenderFn renders a Kubernetes-compliant YAML resource into an
|
|
// Unstructured object, optionally setting the supplied parameters.
|
|
type ResourceRenderFn func(data []byte, p ...Parameter) (*unstructured.Unstructured, error)
|
|
|
|
// Render the supplied Kubernetes YAML resource.
|
|
func (fn ResourceRenderFn) Render(data []byte, p ...Parameter) (*unstructured.Unstructured, error) {
|
|
return fn(data, p...)
|
|
}
|
|
|
|
func renderWorkload(data []byte, p ...Parameter) (*unstructured.Unstructured, error) {
|
|
// TODO(negz): Is there a better decoder to use here?
|
|
w := &fieldpath.Paved{}
|
|
if err := json.Unmarshal(data, w); err != nil {
|
|
return nil, errors.Wrap(err, errUnmarshalWorkload)
|
|
}
|
|
|
|
for _, param := range p {
|
|
for _, path := range param.FieldPaths {
|
|
// TODO(negz): Infer parameter type from workload OpenAPI schema.
|
|
switch param.Value.Type {
|
|
case intstr.String:
|
|
if err := w.SetString(path, param.Value.StrVal); err != nil {
|
|
return nil, errors.Wrapf(err, errFmtSetParam, param.Name)
|
|
}
|
|
case intstr.Int:
|
|
if err := w.SetNumber(path, float64(param.Value.IntVal)); err != nil {
|
|
return nil, errors.Wrapf(err, errFmtSetParam, param.Name)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return &unstructured.Unstructured{Object: w.UnstructuredContent()}, nil
|
|
}
|
|
|
|
func renderTrait(data []byte, _ ...Parameter) (*unstructured.Unstructured, error) {
|
|
// TODO(negz): Is there a better decoder to use here?
|
|
u := &unstructured.Unstructured{}
|
|
if err := json.Unmarshal(data, u); err != nil {
|
|
return nil, errors.Wrap(err, errUnmarshalTrait)
|
|
}
|
|
return u, nil
|
|
}
|
|
|
|
// A Parameter may be used to set the supplied paths to the supplied value.
|
|
type Parameter struct {
|
|
// Name of this parameter.
|
|
Name string
|
|
|
|
// Value of this parameter.
|
|
Value intstr.IntOrString
|
|
|
|
// FieldPaths that should be set to this parameter's value.
|
|
FieldPaths []string
|
|
}
|
|
|
|
// A ParameterResolver resolves the parameters accepted by a component and the
|
|
// parameter values supplied to a component into configured parameters.
|
|
type ParameterResolver interface {
|
|
Resolve([]v1alpha2.ComponentParameter, []v1alpha2.ComponentParameterValue) ([]Parameter, error)
|
|
}
|
|
|
|
// A ParameterResolveFn resolves the parameters accepted by a component and the
|
|
// parameter values supplied to a component into configured parameters.
|
|
type ParameterResolveFn func([]v1alpha2.ComponentParameter, []v1alpha2.ComponentParameterValue) ([]Parameter, error)
|
|
|
|
// Resolve the supplied parameters.
|
|
func (fn ParameterResolveFn) Resolve(cp []v1alpha2.ComponentParameter, cpv []v1alpha2.ComponentParameterValue) ([]Parameter, error) {
|
|
return fn(cp, cpv)
|
|
}
|
|
|
|
func resolve(cp []v1alpha2.ComponentParameter, cpv []v1alpha2.ComponentParameterValue) ([]Parameter, error) {
|
|
supported := make(map[string]bool)
|
|
for _, v := range cp {
|
|
supported[v.Name] = true
|
|
}
|
|
|
|
set := make(map[string]*Parameter)
|
|
for _, v := range cpv {
|
|
if !supported[v.Name] {
|
|
return nil, errors.Errorf(errFmtUnsupportedParam, v.Name)
|
|
}
|
|
set[v.Name] = &Parameter{Name: v.Name, Value: v.Value}
|
|
}
|
|
|
|
for _, p := range cp {
|
|
_, ok := set[p.Name]
|
|
if !ok && p.Required != nil && *p.Required {
|
|
// This parameter is required, but not set.
|
|
return nil, errors.Errorf(errFmtRequiredParam, p.Name)
|
|
}
|
|
if !ok {
|
|
// This parameter is not required, and not set.
|
|
continue
|
|
}
|
|
|
|
set[p.Name].FieldPaths = p.FieldPaths
|
|
}
|
|
|
|
params := make([]Parameter, 0, len(set))
|
|
for _, p := range set {
|
|
params = append(params, *p)
|
|
}
|
|
|
|
return params, nil
|
|
}
|
|
|
|
func addDataOutputsToDAG(dag *dag, outs []v1alpha2.DataOutput, obj *unstructured.Unstructured) {
|
|
for _, out := range outs {
|
|
r := &corev1.ObjectReference{
|
|
APIVersion: obj.GetAPIVersion(),
|
|
Kind: obj.GetKind(),
|
|
Name: obj.GetName(),
|
|
Namespace: obj.GetNamespace(),
|
|
FieldPath: out.FieldPath,
|
|
}
|
|
dag.AddSource(out.Name, r, out.Conditions)
|
|
}
|
|
}
|
|
|
|
func (r *components) handleDependency(ctx context.Context, w *Workload, acc v1alpha2.ApplicationConfigurationComponent, dag *dag, ac *v1alpha2.ApplicationConfiguration) ([]v1alpha2.UnstaifiedDependency, error) {
|
|
uds := make([]v1alpha2.UnstaifiedDependency, 0)
|
|
unstructuredAC, err := util.Object2Unstructured(ac)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "handleDataInput by convert AppConfig (%s) to unstructured object failed", ac.Name)
|
|
}
|
|
// Record the dataOutput with ready conditions
|
|
var unsatisfied []v1alpha2.UnstaifiedDependency
|
|
unsatisfied, w.DataOutputs = r.handleDataOutput(ctx, acc.DataOutputs, dag, unstructuredAC)
|
|
if len(unsatisfied) != 0 {
|
|
uds = append(uds, unsatisfied...)
|
|
}
|
|
unsatisfied, err = r.handleDataInput(ctx, acc.DataInputs, dag, w.Workload, unstructuredAC)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "handleDataInput for workload (%s/%s) failed", w.Workload.GetNamespace(), w.Workload.GetName())
|
|
}
|
|
if len(unsatisfied) != 0 {
|
|
uds = append(uds, unsatisfied...)
|
|
w.HasDep = true
|
|
} else {
|
|
w.DataInputs = acc.DataInputs
|
|
}
|
|
|
|
for i, ct := range acc.Traits {
|
|
trait := w.Traits[i]
|
|
unsatisfied, trait.DataOutputs = r.handleDataOutput(ctx, ct.DataOutputs, dag, unstructuredAC)
|
|
if len(unsatisfied) != 0 {
|
|
uds = append(uds, unsatisfied...)
|
|
}
|
|
unsatisfied, err := r.handleDataInput(ctx, ct.DataInputs, dag, &trait.Object, unstructuredAC)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "handleDataInput for trait (%s/%s) failed", trait.Object.GetNamespace(), trait.Object.GetName())
|
|
}
|
|
if len(unsatisfied) != 0 {
|
|
uds = append(uds, unsatisfied...)
|
|
trait.HasDep = true
|
|
} else {
|
|
trait.DataInputs = ct.DataInputs
|
|
}
|
|
}
|
|
return uds, nil
|
|
}
|
|
|
|
func makeUnsatisfiedDependency(obj *unstructured.Unstructured, s *dagSource, toPaths []string, reason string) v1alpha2.UnstaifiedDependency {
|
|
return v1alpha2.UnstaifiedDependency{
|
|
Reason: reason,
|
|
From: v1alpha2.DependencyFromObject{
|
|
ObjectReference: corev1.ObjectReference{
|
|
APIVersion: s.ObjectRef.APIVersion,
|
|
Kind: s.ObjectRef.Kind,
|
|
Name: s.ObjectRef.Name,
|
|
},
|
|
FieldPath: s.ObjectRef.FieldPath,
|
|
},
|
|
To: v1alpha2.DependencyToObject{
|
|
ObjectReference: corev1.ObjectReference{
|
|
APIVersion: obj.GetAPIVersion(),
|
|
Kind: obj.GetKind(),
|
|
Name: obj.GetName(),
|
|
},
|
|
FieldPaths: toPaths,
|
|
},
|
|
}
|
|
}
|
|
|
|
func (r *components) handleDataOutput(ctx context.Context, outputs []v1alpha2.DataOutput, dag *dag, ac *unstructured.Unstructured) ([]v1alpha2.UnstaifiedDependency, map[string]v1alpha2.DataOutput) {
|
|
uds := make([]v1alpha2.UnstaifiedDependency, 0)
|
|
outputMap := make(map[string]v1alpha2.DataOutput)
|
|
for _, out := range outputs {
|
|
if reflect.DeepEqual(out.OutputStore, v1alpha2.StoreReference{}) {
|
|
continue
|
|
}
|
|
s, ok := dag.Sources[out.Name]
|
|
if !ok {
|
|
continue
|
|
}
|
|
// the outputStore is considered ready when all conditions are ready
|
|
allConditionsReady := true
|
|
for _, oper := range out.OutputStore.Operations {
|
|
newS := &dagSource{
|
|
ObjectRef: &corev1.ObjectReference{
|
|
APIVersion: s.ObjectRef.APIVersion,
|
|
Kind: s.ObjectRef.Kind,
|
|
Name: s.ObjectRef.Name,
|
|
Namespace: ac.GetNamespace(),
|
|
FieldPath: oper.ValueFrom.FieldPath,
|
|
},
|
|
Conditions: oper.Conditions,
|
|
}
|
|
_, ready, reason, err := r.getDataInput(ctx, newS, ac, false)
|
|
if err != nil || !ready {
|
|
if err == nil {
|
|
outObj := &unstructured.Unstructured{}
|
|
outObj.SetGroupVersionKind(out.OutputStore.GroupVersionKind())
|
|
outObj.SetName(out.OutputStore.Name)
|
|
toPath := oper.ToFieldPath
|
|
if len(oper.ToDataPath) != 0 {
|
|
toPath = toPath + "(" + oper.ToDataPath + ")"
|
|
}
|
|
uds = append(uds, makeUnsatisfiedDependency(outObj, newS, []string{toPath}, reason))
|
|
}
|
|
allConditionsReady = false
|
|
break
|
|
}
|
|
}
|
|
if allConditionsReady {
|
|
outputMap[out.Name] = out
|
|
}
|
|
}
|
|
return uds, outputMap
|
|
}
|
|
|
|
func (r *components) handleDataInput(ctx context.Context, ins []v1alpha2.DataInput, dag *dag, obj, ac *unstructured.Unstructured) ([]v1alpha2.UnstaifiedDependency, error) {
|
|
uds := make([]v1alpha2.UnstaifiedDependency, 0)
|
|
for _, in := range ins {
|
|
if !reflect.DeepEqual(in.ValueFrom, v1alpha2.DataInputValueFrom{}) && len(strings.TrimSpace(in.ValueFrom.DataOutputName)) != 0 {
|
|
dep, err := r.handleDataOutputConds(ctx, in, dag, obj, ac)
|
|
if dep != nil {
|
|
uds = append(uds, *dep)
|
|
return uds, err
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if !reflect.DeepEqual(in.InputStore, v1alpha2.StoreReference{}) {
|
|
dep, err := r.handleDataStoreConds(ctx, in, obj, ac)
|
|
if dep != nil {
|
|
uds = append(uds, *dep)
|
|
return uds, err
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
if len(in.Conditions) != 0 {
|
|
dep, err := r.handleDataInputConds(ctx, in, dag, obj, ac)
|
|
if dep != nil {
|
|
uds = append(uds, *dep)
|
|
return uds, err
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
return uds, nil
|
|
}
|
|
|
|
func (r *components) handleDataOutputConds(ctx context.Context, in v1alpha2.DataInput, dag *dag, obj, ac *unstructured.Unstructured) (*v1alpha2.UnstaifiedDependency, error) {
|
|
s, ok := dag.Sources[in.ValueFrom.DataOutputName]
|
|
if !ok {
|
|
return nil, errors.Wrapf(ErrDataOutputNotExist, "DataOutputName (%s)", in.ValueFrom.DataOutputName)
|
|
}
|
|
val, ready, reason, err := r.getDataInput(ctx, s, ac, false)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "getDataInput failed")
|
|
}
|
|
if !ready {
|
|
dep := makeUnsatisfiedDependency(obj, s, in.ToFieldPaths, reason)
|
|
return &dep, nil
|
|
}
|
|
err = fillDataInputValue(obj, in.ToFieldPaths, val, in.StrategyMergeKeys)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "fillDataInputValue failed")
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
func (r *components) handleDataStoreConds(ctx context.Context, in v1alpha2.DataInput, obj, ac *unstructured.Unstructured) (*v1alpha2.UnstaifiedDependency, error) {
|
|
for _, oper := range in.InputStore.Operations {
|
|
s := &dagSource{
|
|
ObjectRef: &corev1.ObjectReference{
|
|
APIVersion: in.InputStore.APIVersion,
|
|
Kind: in.InputStore.Kind,
|
|
Name: in.InputStore.Name,
|
|
// according current implementation, outputRef use the namespace of workload which is set with the namespace of ac. So it's ok to use ac.GetNamespace() here.
|
|
// obj.GetNamespace() may be empty when obj has not been created.
|
|
Namespace: ac.GetNamespace(),
|
|
FieldPath: oper.ValueFrom.FieldPath,
|
|
},
|
|
Conditions: oper.Conditions,
|
|
}
|
|
_, ready, reason, err := r.getDataInput(ctx, s, ac, false)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "getDataInput failed")
|
|
}
|
|
if !ready {
|
|
toPath := oper.ToFieldPath
|
|
if len(oper.ToDataPath) != 0 {
|
|
toPath = toPath + "(" + oper.ToDataPath + ")"
|
|
}
|
|
dep := makeUnsatisfiedDependency(obj, s, []string{toPath}, reason)
|
|
return &dep, nil
|
|
}
|
|
}
|
|
return nil, nil
|
|
}
|
|
func (r *components) handleDataInputConds(ctx context.Context, in v1alpha2.DataInput, dag *dag, obj, ac *unstructured.Unstructured) (*v1alpha2.UnstaifiedDependency, error) {
|
|
_, ok := dag.Sources[in.ValueFrom.DataOutputName]
|
|
if !ok {
|
|
return nil, errors.Wrapf(ErrDataOutputNotExist, "DataOutputName (%s)", in.ValueFrom.DataOutputName)
|
|
}
|
|
s := &dagSource{
|
|
ObjectRef: &corev1.ObjectReference{
|
|
APIVersion: obj.GetAPIVersion(),
|
|
Kind: obj.GetKind(),
|
|
Name: obj.GetName(),
|
|
// according current implementation, outputRef use the namespace of workload which is set with the namespace of ac. So it's ok to use ac.GetNamespace() here.
|
|
// obj.GetNamespace() may be empty when obj has not been created.
|
|
Namespace: ac.GetNamespace(),
|
|
},
|
|
Conditions: in.Conditions,
|
|
}
|
|
_, ready, reason, err := r.getDataInput(ctx, s, ac, true)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "getDataInput failed")
|
|
}
|
|
if !ready {
|
|
dep := makeUnsatisfiedDependency(obj, dag.Sources[in.ValueFrom.DataOutputName], in.ToFieldPaths, "DataInputs Conditions: "+reason)
|
|
return &dep, nil
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
func (r *components) getDataInput(ctx context.Context, s *dagSource, ac *unstructured.Unstructured, ignoreNotFound bool) (interface{}, bool, string, error) {
|
|
obj := s.ObjectRef
|
|
key := types.NamespacedName{
|
|
Namespace: obj.Namespace,
|
|
Name: obj.Name,
|
|
}
|
|
// If obj.FieldPath is empty and the length of dagSource's Conditions is 0, return true
|
|
if len(obj.FieldPath) == 0 && len(s.Conditions) == 0 {
|
|
return nil, true, "", nil
|
|
}
|
|
u := &unstructured.Unstructured{}
|
|
u.SetGroupVersionKind(obj.GroupVersionKind())
|
|
err := r.client.Get(ctx, key, u)
|
|
if err != nil {
|
|
if resource.IgnoreNotFound(err) == nil && ignoreNotFound {
|
|
return nil, true, "", nil
|
|
}
|
|
reason := fmt.Sprintf("failed to get object (%s)", key.String())
|
|
return nil, false, reason, errors.Wrap(resource.IgnoreNotFound(err), reason)
|
|
}
|
|
paved := fieldpath.Pave(u.UnstructuredContent())
|
|
pavedAC := fieldpath.Pave(ac.UnstructuredContent())
|
|
rawval, err := paved.GetValue(obj.FieldPath)
|
|
if err != nil {
|
|
if fieldpath.IsNotFound(err) {
|
|
return "", false, fmt.Sprintf("%s not found in object", obj.FieldPath), nil
|
|
}
|
|
err = fmt.Errorf("failed to get field value (%s) in object (%s): %w", obj.FieldPath, key.String(), err)
|
|
return nil, false, err.Error(), err
|
|
}
|
|
|
|
var ok bool
|
|
var reason string
|
|
switch val := rawval.(type) {
|
|
case string:
|
|
// For string input we will:
|
|
// - check its value not empty if no condition is given.
|
|
// - check its value against conditions if no field path is specified.
|
|
ok, reason = matchValue(s.Conditions, val, paved, pavedAC)
|
|
default:
|
|
ok, reason = checkConditions(s.Conditions, paved, nil, pavedAC)
|
|
}
|
|
if !ok {
|
|
return nil, false, reason, nil
|
|
}
|
|
|
|
return rawval, true, "", nil
|
|
}
|
|
|
|
func isControlledByApp(ac *v1alpha2.ApplicationConfiguration) bool {
|
|
for _, owner := range ac.GetOwnerReferences() {
|
|
if owner.APIVersion == v1beta1.SchemeGroupVersion.String() && owner.Kind == v1beta1.ApplicationKind &&
|
|
owner.Controller != nil && *owner.Controller {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// getOwnerFromAC will check and get the real owner, if the owner is Application, it will use ApplicationContext as owner
|
|
// or it will make the AC as the owner
|
|
func getOwnerFromAC(ac *v1alpha2.ApplicationConfiguration) *metav1.OwnerReference {
|
|
return metav1.NewControllerRef(ac, v1alpha2.ApplicationConfigurationGroupVersionKind)
|
|
}
|
|
|
|
func matchValue(conds []v1alpha2.ConditionRequirement, val string, paved, ac *fieldpath.Paved) (bool, string) {
|
|
// If no condition is specified, it is by default to check value not empty.
|
|
if len(conds) == 0 {
|
|
if val == "" {
|
|
return false, "value should not be empty"
|
|
}
|
|
return true, ""
|
|
}
|
|
|
|
return checkConditions(conds, paved, &val, ac)
|
|
}
|
|
|
|
func getCheckVal(m v1alpha2.ConditionRequirement, paved *fieldpath.Paved, val *string) (string, error) {
|
|
var checkVal string
|
|
switch {
|
|
case m.FieldPath != "":
|
|
return paved.GetString(m.FieldPath)
|
|
case val != nil:
|
|
checkVal = *val
|
|
default:
|
|
return "", errors.New("FieldPath not specified")
|
|
}
|
|
return checkVal, nil
|
|
}
|
|
|
|
func getExpectVal(m v1alpha2.ConditionRequirement, ac *fieldpath.Paved) (string, error) {
|
|
if m.Value != "" {
|
|
return m.Value, nil
|
|
}
|
|
if m.ValueFrom.FieldPath == "" || ac == nil {
|
|
return "", nil
|
|
}
|
|
var err error
|
|
value, err := ac.GetString(m.ValueFrom.FieldPath)
|
|
if err != nil {
|
|
return "", fmt.Errorf("get valueFrom.fieldPath fail: %w", err)
|
|
}
|
|
return value, nil
|
|
}
|
|
|
|
func checkConditions(conds []v1alpha2.ConditionRequirement, paved *fieldpath.Paved, val *string, ac *fieldpath.Paved) (bool, string) {
|
|
for _, m := range conds {
|
|
checkVal, err := getCheckVal(m, paved, val)
|
|
if err != nil {
|
|
return false, fmt.Sprintf("can't get value to check %v", err)
|
|
}
|
|
m.Value, err = getExpectVal(m, ac)
|
|
if err != nil {
|
|
return false, err.Error()
|
|
}
|
|
|
|
switch m.Operator {
|
|
case v1alpha2.ConditionEqual:
|
|
if m.Value != checkVal {
|
|
return false, fmt.Sprintf("got(%v) expected to be %v", checkVal, m.Value)
|
|
}
|
|
case v1alpha2.ConditionNotEqual:
|
|
if m.Value == checkVal {
|
|
return false, fmt.Sprintf("got(%v) expected not to be %v", checkVal, m.Value)
|
|
}
|
|
case v1alpha2.ConditionNotEmpty:
|
|
if checkVal == "" {
|
|
return false, "value should not be empty"
|
|
}
|
|
}
|
|
}
|
|
return true, ""
|
|
}
|
|
|
|
// GetTraitName return trait name
|
|
func getTraitName(ac *v1alpha2.ApplicationConfiguration, componentName string,
|
|
ct *v1alpha2.ComponentTrait, t *unstructured.Unstructured, traitDef *v1alpha2.TraitDefinition) string {
|
|
var traitName, apiVersion, kind string
|
|
// we forbid the trait name in the template if the applicationConfiguration is generated by application
|
|
if len(t.GetName()) > 0 && !isControlledByApp(ac) {
|
|
return t.GetName()
|
|
}
|
|
|
|
apiVersion = t.GetAPIVersion()
|
|
kind = t.GetKind()
|
|
|
|
traitType := traitDef.Name
|
|
if strings.Contains(traitType, ".") {
|
|
traitType = strings.Split(traitType, ".")[0]
|
|
}
|
|
|
|
for _, w := range ac.Status.Workloads {
|
|
if w.ComponentName != componentName {
|
|
continue
|
|
}
|
|
for _, trait := range w.Traits {
|
|
if trait.Reference.APIVersion == apiVersion && trait.Reference.Kind == kind {
|
|
traitName = trait.Reference.Name
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(traitName) == 0 {
|
|
traitName = util.GenTraitName(componentName, ct.DeepCopy(), traitType)
|
|
}
|
|
|
|
return traitName
|
|
}
|
|
|
|
// getExistingWorkload tries to retrieve the currently running workload
|
|
func (r *components) getExistingWorkload(ctx context.Context, ac *v1alpha2.ApplicationConfiguration, c *v1alpha2.Component, w *unstructured.Unstructured) (*unstructured.Unstructured, error) {
|
|
var workloadName string
|
|
existingWorkload := &unstructured.Unstructured{}
|
|
for _, component := range ac.Status.Workloads {
|
|
if component.ComponentName != c.GetName() {
|
|
continue
|
|
}
|
|
workloadName = component.Reference.Name
|
|
}
|
|
if workloadName != "" {
|
|
objectKey := client.ObjectKey{Namespace: ac.GetNamespace(), Name: workloadName}
|
|
existingWorkload.SetAPIVersion(w.GetAPIVersion())
|
|
existingWorkload.SetKind(w.GetKind())
|
|
err := r.client.Get(ctx, objectKey, existingWorkload)
|
|
if err != nil {
|
|
return nil, client.IgnoreNotFound(err)
|
|
}
|
|
}
|
|
return existingWorkload, nil
|
|
}
|
|
|
|
// discoverHelmModuleWorkload will get the workload created by flux/helm-controller
|
|
func discoverHelmModuleWorkload(ctx context.Context, c client.Reader, comp *v1alpha2.Component, ns string) (*unstructured.Unstructured, error) {
|
|
if comp == nil || comp.Spec.Helm == nil {
|
|
return nil, errors.New("the component has no valid helm module")
|
|
}
|
|
|
|
rls, err := util.RawExtension2Unstructured(&comp.Spec.Helm.Release)
|
|
if err != nil {
|
|
return nil, 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 nil, 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], "-")
|
|
}
|
|
}
|
|
|
|
wl, err := util.RawExtension2Unstructured(&comp.Spec.Workload)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "cannot get workload from component")
|
|
}
|
|
|
|
if err := c.Get(ctx, client.ObjectKey{Namespace: ns, Name: qualifiedWorkloadName}, wl); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// check it's created by helm and match the release info
|
|
annots := wl.GetAnnotations()
|
|
labels := wl.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 nil, err
|
|
}
|
|
|
|
return wl, nil
|
|
}
|