Skip to content

Commit

Permalink
Add OAM workload translators
Browse files Browse the repository at this point in the history
Signed-off-by: hasheddan <georgedanielmangum@gmail.com>
  • Loading branch information
hasheddan committed Mar 24, 2020
1 parent d15825b commit 86eb3b0
Show file tree
Hide file tree
Showing 9 changed files with 1,307 additions and 2 deletions.
3 changes: 2 additions & 1 deletion pkg/oam/trait/modify.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ var (
deploymentKind = reflect.TypeOf(appsv1.Deployment{}).Name()
)

// DeploymentFromKubeAppAccessor gets deployments from a KubernetesApplication.
// DeploymentFromKubeAppAccessor finds deployments in a KubernetesApplication
// and applies the supplied modifier function to them.
func DeploymentFromKubeAppAccessor(ctx context.Context, obj runtime.Object, t resource.Trait, m trait.ModifyFn) error {
a, ok := obj.(*workloadv1alpha1.KubernetesApplication)
if !ok {
Expand Down
92 changes: 92 additions & 0 deletions pkg/oam/workload/apply.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
Copyright 2020 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 workload

import (
"context"
"encoding/json"

jsonpatch "github.com/evanphx/json-patch"
"github.com/pkg/errors"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"

"github.com/crossplane/crossplane-runtime/pkg/resource"

workloadv1alpha1 "github.com/crossplane/crossplane/apis/workload/v1alpha1"
)

var (
errNotKubeApp = "object is not a KubernetesApplication"
errMergeKubeAppTemplates = "cannot merge KubernetesApplicationResourceTemplates"
)

type template struct {
gvk schema.GroupVersionKind
name string
}

// KubeAppApplyOption ensures resource templates are merged instead of replaced
// before patch if they have the same name and GroupVersionKind. We must merge
// the current and desired templates prior to submitting a Patch to the API
// server because KubernetesApplicationResourceTemplates are stored as an array
// in the KubernetesApplication. This means that entire templates will be
// replaced when a single field is different, per
// https://tools.ietf.org/html/rfc7386. We instead patch each of the resource
// templates individually before passing along the entire KubernetesApplication
// to resource.Apply.
func KubeAppApplyOption() resource.ApplyOption {
return func(_ context.Context, current, desired runtime.Object) error {
c, ok := current.(*workloadv1alpha1.KubernetesApplication)
if !ok {
return errors.New(errNotKubeApp)
}
d, ok := desired.(*workloadv1alpha1.KubernetesApplication)
if !ok {
return errors.New(errNotKubeApp)
}

index := make(map[template]int)
for i, t := range d.Spec.ResourceTemplates {
temp := &unstructured.Unstructured{}
if err := json.Unmarshal(t.Spec.Template.Raw, temp); err != nil {
return errors.Wrap(err, errMergeKubeAppTemplates)
}
index[template{gvk: temp.GroupVersionKind(), name: t.GetName()}] = i
}

for _, t := range c.Spec.ResourceTemplates {
temp := &unstructured.Unstructured{}
if err := json.Unmarshal(t.Spec.Template.Raw, temp); err != nil {
return errors.Wrap(err, errMergeKubeAppTemplates)
}
i, ok := index[template{gvk: temp.GroupVersionKind(), name: t.GetName()}]
if !ok {
continue
}

merged, err := jsonpatch.MergePatch(t.Spec.Template.Raw, d.Spec.ResourceTemplates[i].Spec.Template.Raw)
if err != nil {
return errors.Wrap(err, errMergeKubeAppTemplates)
}
d.Spec.ResourceTemplates[i].Spec.Template = runtime.RawExtension{Raw: merged}
}

return nil
}
}
165 changes: 165 additions & 0 deletions pkg/oam/workload/apply_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
/*
Copyright 2020 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 workload

import (
"context"
"encoding/json"
"testing"
"time"

"github.com/google/go-cmp/cmp"
"github.com/pkg/errors"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"

"github.com/crossplane/crossplane-runtime/pkg/resource"
"github.com/crossplane/crossplane-runtime/pkg/test"

workloadv1alpha1 "github.com/crossplane/crossplane/apis/workload/v1alpha1"
)

var replicas = int32(3)

type kubeAppModifier func(*workloadv1alpha1.KubernetesApplication)

func kaWithTemplate(name string, o runtime.Object) kubeAppModifier {
return func(a *workloadv1alpha1.KubernetesApplication) {
b, _ := json.Marshal(o)
a.Spec.ResourceTemplates = append(a.Spec.ResourceTemplates, workloadv1alpha1.KubernetesApplicationResourceTemplate{
ObjectMeta: metav1.ObjectMeta{
Name: name,
CreationTimestamp: metav1.NewTime(time.Date(0, 0, 0, 0, 0, 0, 0, time.Local)),
},
Spec: workloadv1alpha1.KubernetesApplicationResourceSpec{
Template: runtime.RawExtension{Raw: b},
},
})
}
}

func kubeApp(mod ...kubeAppModifier) *workloadv1alpha1.KubernetesApplication {
a := &workloadv1alpha1.KubernetesApplication{
ObjectMeta: metav1.ObjectMeta{
Name: "cool-kapp",
},
}

for _, m := range mod {
m(a)
}

return a
}

var _ resource.ApplyOption = KubeAppApplyOption()

func TestKubeAppApplyOption(t *testing.T) {
type args struct {
c runtime.Object
d runtime.Object
}

type want struct {
o runtime.Object
err error
}

cases := map[string]struct {
reason string
args args
want want
}{
"NotAKubernetesApplication": {
reason: "An error should be returned if the object is not a KubernetesApplication",
args: args{
c: &corev1.Namespace{},
d: &corev1.Namespace{},
},
want: want{
o: &corev1.Namespace{},
err: errors.New(errNotKubeApp),
},
},
"PatchedNoOverwrite": {
reason: "If existing and desired have the same name and kind of a template, non-array fields in templates should not be overwritten in patch",
args: args{
c: kubeApp(kaWithTemplate("cool-temp", deployment(dmWithReplicas(&replicas)))),
d: kubeApp(kaWithTemplate("cool-temp", deployment())),
},
want: want{
o: kubeApp(kaWithTemplate("cool-temp", deployment(dmWithReplicas(&replicas)))),
},
},
"PatchedRemoveResource": {
reason: "If existing and desired have different template names, the existing template should be overwritten by the desired",
args: args{
c: kubeApp(kaWithTemplate("cool-temp", deployment()), kaWithTemplate("nice-temp", deployment())),
d: kubeApp(kaWithTemplate("cool-temp", deployment())),
},
want: want{
o: kubeApp(kaWithTemplate("cool-temp", deployment())),
},
},
"PatchedAddResource": {
reason: "If existing and desired have different template names, the existing template should be overwritten by the desired",
args: args{
c: kubeApp(kaWithTemplate("cool-temp", deployment())),
d: kubeApp(kaWithTemplate("cool-temp", deployment()), kaWithTemplate("nice-temp", deployment())),
},
want: want{
o: kubeApp(kaWithTemplate("cool-temp", deployment()), kaWithTemplate("nice-temp", deployment())),
},
},
"PatchedOverwrite": {
reason: "If existing and desired have different template names, the existing template should be overwritten by the desired",
args: args{
c: kubeApp(kaWithTemplate("nice-temp", deployment())),
d: kubeApp(kaWithTemplate("cool-temp", deployment())),
},
want: want{
o: kubeApp(kaWithTemplate("cool-temp", deployment())),
},
},
"PatchedPartialOverwrite": {
reason: "If existing and desired have the same name and kind of a template, array fields in templates should be overwritten in patch",
args: args{
c: kubeApp(kaWithTemplate("cool-temp", deployment(dmWithReplicas(&replicas), dmWithContainerPorts(replicas)))),
d: kubeApp(kaWithTemplate("cool-temp", deployment(dmWithReplicas(&replicas)))),
},
want: want{
o: kubeApp(kaWithTemplate("cool-temp", deployment(dmWithReplicas(&replicas)))),
},
},
}

for name, tc := range cases {
t.Run(name, func(t *testing.T) {
err := KubeAppApplyOption()(context.Background(), tc.args.c, tc.args.d)
if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
t.Errorf("\n%s\nKubeAppApplyOption(...): -want error, +got error\n%s\n", tc.reason, diff)
}

o, _ := runtime.DefaultUnstructuredConverter.ToUnstructured(tc.want.o)
d, _ := runtime.DefaultUnstructuredConverter.ToUnstructured(tc.args.d)
if diff := cmp.Diff(o, d); diff != "" {
t.Errorf("\n%s\nKubeAppApplyOption(...): -want, +got\n%s\n", tc.reason, diff)
}
})
}
}
Loading

0 comments on commit 86eb3b0

Please sign in to comment.