Skip to content
This repository has been archived by the owner on Jun 8, 2022. It is now read-only.

implement Trait patch back before workload emitted #134

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions apis/core/v1alpha2/core_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ type WorkloadDefinitionList struct {

// A TraitDefinitionSpec defines the desired state of a TraitDefinition.
type TraitDefinitionSpec struct {
// Flags whether the trait is separate from the underlying workload
// +optional
Patch bool `json:"patch,omitempty"`

// Reference to the CustomResourceDefinition that defines this trait kind.
Reference DefinitionReference `json:"definitionRef"`

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ spec:
description: Extension is used for extension needs by OAM platform
builders
type: object
patch:
description: Flags whether the trait is separate from the underlying
workload
type: boolean
revisionEnabled:
description: Revision indicates whether a trait is aware of component
revision
Expand Down
123 changes: 123 additions & 0 deletions examples/trait-separated/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
## How do I add trait with patch fragments

### Prerequisites
* [Install OAM Runtime](https://github.com/crossplane/oam-kubernetes-runtime#install-oam-runtime)


### Install traits with patch fragments

* Install the Trait Definition

```
apiVersion: core.oam.dev/v1alpha2
kind: TraitDefinition
metadata:
name: nodeselectors.extended.oam.dev
spec:
patch: true
appliesToWorkloads:
- core.oam.dev/v1alpha2.ContainerizedWorkload
definitionRef:
name: nodeselectors.extended.oam.dev
```
* Install NodeSelector Controller


* Add traits with patch fragments to the ApplicationConfiguration

`sample_application_config.yaml`

```
apiVersion: core.oam.dev/v1alpha2
kind: ApplicationConfiguration
metadata:
name: example-appconfig
spec:
components:
- componentName: example-component
parameterValues:
- name: instance-name
value: example-appconfig-workload
- name: image
value: wordpress:php7.2
traits:
- trait:
apiVersion: core.oam.dev/v1alpha2
kind: ManualScalerTrait
metadata:
name: example-appconfig-trait
spec:
replicaCount: 3
- trait:
apiVersion: extended.oam.dev/v1alpha2
kind: NodeSelector
metadata:
name: example-appconfig-nodeselector
spec:
disktype: ssd
scopes:
- scopeRef:
apiVersion: core.oam.dev/v1alpha2
kind: HealthScope
name: example-health-scope
```


* Apply a sample application configuration

```
# kubectl apply -f sample_application_config.yaml

```
* Check the status of the Trait and confirm whether the value of patchConfig is set successfully

```
# kubectl get NodeSelector example-appconfig-nodeselector
```

```
apiVersion: extended.oam.dev/v1alpha2
kind: NodeSelector
metadata:
name: example-component-trait-6db9578cfd
spec:
disktype: ssd
status:
patchConfig: nodeselector-cm
phase: Ready
```


### Query ConfigMap content to confirm whether the Patch data is correct

```
# kubectl get cm nodeselector-cm -o yaml
```

```
apiVersion: v1
kind: ConfigMap
metadata:
name: nodeselector-cm
data:
patch: |
{
"spec": {
"template": {
"spec": {
"nodeSelector": {
"disktype": "ssd"
}
}
}
}
}

```

### Check the Spec content of the Workload

```
# kubectl get deployment example-appconfig-workload -o yaml

```
4 changes: 3 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,19 @@ go 1.13
require (
github.com/crossplane/crossplane-runtime v0.8.0
github.com/davecgh/go-spew v1.1.1
github.com/evanphx/json-patch v4.5.0+incompatible
github.com/gertd/go-pluralize v0.1.7
github.com/go-logr/logr v0.1.0
github.com/google/go-cmp v0.4.0
github.com/imdario/mergo v0.3.7
github.com/json-iterator/go v1.1.8
github.com/onsi/ginkgo v1.11.0
github.com/onsi/gomega v1.8.1
github.com/pkg/errors v0.9.1
github.com/rs/xid v1.2.1
github.com/stretchr/testify v1.4.0
go.uber.org/zap v1.10.0
golang.org/x/tools v0.0.0-20200630223951-c138986dd9b9 // indirect
golang.org/x/tools v0.0.0-20200630223951-c138986dd9b9
gopkg.in/natefinch/lumberjack.v2 v2.0.0
k8s.io/api v0.18.5
k8s.io/apiextensions-apiserver v0.18.2
Expand Down
121 changes: 102 additions & 19 deletions pkg/controller/v1alpha2/applicationconfiguration/render.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,12 @@ import (
"context"
"encoding/json"
"fmt"
"strings"

runtimev1alpha1 "github.com/crossplane/crossplane-runtime/apis/core/v1alpha1"
"github.com/crossplane/crossplane-runtime/pkg/fieldpath"
"github.com/crossplane/crossplane-runtime/pkg/resource"
jsonpatch "github.com/evanphx/json-patch"
"github.com/pkg/errors"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
Expand All @@ -44,15 +46,24 @@ const (

// 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"
errSetValueForField = "can not set value %q for fieldPath %q"
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"
errSetValueForField = "can not set value %q for fieldPath %q"
errGetConfigMapValueForName = "can not get configMap data for configMapName %q"
errNoSuchField = "status: no such field"
)

const (
//StatusKey stands for the status field of the spec
StatusKey = "status"
patchConfigKey = "patchConfig"
patchKey = "patch"
)

var (
Expand Down Expand Up @@ -88,18 +99,21 @@ func (r *components) Render(ctx context.Context, ac *v1alpha2.ApplicationConfigu
workloads := make([]*Workload, 0, len(ac.Spec.Components))
dag := newDAG()

newComponents := make([]v1alpha2.ApplicationConfigurationComponent, 0)
for _, acc := range ac.Spec.Components {
w, err := r.renderComponent(ctx, acc, ac, dag)
acc := acc
w, err := r.renderComponent(ctx, &acc, ac, dag)
if err != nil {
return nil, nil, err
}

workloads = append(workloads, w)
newComponents = append(newComponents, acc)
}

ds := &v1alpha2.DependencyStatus{}
res := make([]Workload, 0, len(ac.Spec.Components))
for i, acc := range ac.Spec.Components {
res := make([]Workload, 0, len(newComponents))
for i, acc := range newComponents {
unsatisfied, err := r.handleDependency(ctx, workloads[i], acc, dag)
if err != nil {
return nil, nil, err
Expand All @@ -111,11 +125,12 @@ func (r *components) Render(ctx context.Context, ac *v1alpha2.ApplicationConfigu
return res, ds, nil
}

func (r *components) renderComponent(ctx context.Context, acc v1alpha2.ApplicationConfigurationComponent, ac *v1alpha2.ApplicationConfiguration, dag *dag) (*Workload, error) {
func (r *components) renderComponent(ctx context.Context, acc *v1alpha2.ApplicationConfigurationComponent, ac *v1alpha2.ApplicationConfiguration, dag *dag) (*Workload, error) {
if acc.RevisionName != "" {
acc.ComponentName = ExtractComponentName(acc.RevisionName)
}
c, componentRevisionName, err := util.GetComponent(ctx, r.client, acc, ac.GetNamespace())

c, componentRevisionName, err := util.GetComponent(ctx, r.client, *acc, ac.GetNamespace())
if err != nil {
return nil, err
}
Expand All @@ -139,10 +154,17 @@ func (r *components) renderComponent(ctx context.Context, acc v1alpha2.Applicati
traits := make([]*Trait, 0, len(acc.Traits))
traitDefs := make([]v1alpha2.TraitDefinition, 0, len(acc.Traits))
for _, ct := range acc.Traits {
t, traitDef, err := r.renderTrait(ctx, ct, ac, acc.ComponentName, ref, dag)
ct := ct
t, traitDef, err := r.renderTrait(ctx, &ct, ac, acc.ComponentName, ref, dag)
if err != nil {
return nil, err
}
//Render patch data
if traitDef.Spec.Patch {
if err := r.renderComponentFromPatchTrait(ctx, w, acc, t); err != nil {
return nil, err
}
}

// pass through labels and annotation from app-config to trait
r.passThroughObjMeta(ac.ObjectMeta, t)
Expand All @@ -168,26 +190,87 @@ func (r *components) renderComponent(ctx context.Context, acc v1alpha2.Applicati
return &Workload{ComponentName: acc.ComponentName, ComponentRevisionName: componentRevisionName, Workload: w, Traits: traits, Scopes: scopes}, nil
}

func (r *components) renderTrait(ctx context.Context, ct v1alpha2.ComponentTrait, ac *v1alpha2.ApplicationConfiguration,
func (r *components) renderComponentFromPatchTrait(ctx context.Context, w *unstructured.Unstructured, acc *v1alpha2.ApplicationConfigurationComponent, t *unstructured.Unstructured) error {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this one have a unit test? I would also suggest to break this function into multiple pieces (the patch itself can be a function)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I'll add a unit test

dataInput := v1alpha2.DataInput{
ValueFrom: v1alpha2.DataInputValueFrom{
DataOutputName: t.GetName(),
},
}
acc.DataInputs = append(acc.DataInputs, dataInput)
//get trait status
targetTraitObject := &unstructured.Unstructured{}
targetTraitObject.SetGroupVersionKind(t.GroupVersionKind())
if err := r.client.Get(ctx, types.NamespacedName{Name: t.GetName(), Namespace: t.GetNamespace()}, targetTraitObject); err != nil {
return err
}

smap, err := fieldpath.Pave(targetTraitObject.Object).GetStringObject(StatusKey)
if err != nil && !strings.ContainsAny(err.Error(), errNoSuchField) {
return err
}
patchCmName := smap[patchConfigKey]
if len(smap) == 0 || patchCmName == "" {
return nil
}
//patch yaml
nn := types.NamespacedName{Namespace: w.GetNamespace(), Name: patchCmName}
cm := &corev1.ConfigMap{}
var patch string
if err := r.client.Get(ctx, nn, cm); err != nil {
return err
}

if patch = cm.Data[patchKey]; patch == "" {
return errors.Wrapf(err, errGetConfigMapValueForName, nn.Name)
}

workloadJSON, err := w.MarshalJSON()
if err != nil {
return err
}

workloadFullJSON, err := jsonpatch.MergeMergePatches(workloadJSON, []byte(patch))
if err != nil {
return err
}

var mergePatchObject map[string]interface{}
if err = json.Unmarshal(workloadFullJSON, &mergePatchObject); err != nil {
return err
}
w.SetUnstructuredContent(mergePatchObject)
return 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)
}

traitName := getTraitName(ac, componentName, &ct, t)
traitName := getTraitName(ac, componentName, ct, t)

setTraitProperties(t, traitName, ac.GetNamespace(), ref)

traitDef, err := util.FetchTraitDefinition(ctx, r.client, t)
if err != nil {
return nil, nil, errors.Wrapf(err, errFmtGetTraitDefinition, t.GetAPIVersion(), t.GetKind(), t.GetName())
}

if traitDef.Spec.Patch {
r.renderTraitWithOutput(ct, t.GetName())
}
addDataOutputsToDAG(dag, ct.DataOutputs, t)

return t, traitDef, nil
}
func (r *components) renderTraitWithOutput(ct *v1alpha2.ComponentTrait, traitName string) {
//output data
dataOutput := v1alpha2.DataOutput{
Name: traitName,
FieldPath: fmt.Sprintf("%s.%s", StatusKey, patchConfigKey),
}
ct.DataOutputs = append(ct.DataOutputs, dataOutput)
}

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.
Expand Down
Loading