forked from crossplane/crossplane
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Signed-off-by: hasheddan <georgedanielmangum@gmail.com>
- Loading branch information
Showing
9 changed files
with
1,307 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
}) | ||
} | ||
} |
Oops, something went wrong.