Skip to content

Commit

Permalink
Merge pull request crossplane#1358 from hasheddan/oamtrans
Browse files Browse the repository at this point in the history
Add OAM workload and trait translators and modifiers
  • Loading branch information
negz authored Mar 26, 2020
2 parents 4436593 + 1c8310c commit af2a48c
Show file tree
Hide file tree
Showing 16 changed files with 1,781 additions and 0 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ require (
github.com/crossplane/crossplane-runtime v0.6.0
github.com/crossplane/crossplane-tools v0.0.0-20200219001116-bb8b2ce46330
github.com/docker/distribution v2.7.1+incompatible
github.com/evanphx/json-patch v4.5.0+incompatible
github.com/ghodss/yaml v1.0.0
github.com/google/go-cmp v0.3.1
github.com/onsi/gomega v1.7.0
Expand Down
17 changes: 17 additions & 0 deletions pkg/oam/trait/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
Copyright 2019 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 trait
17 changes: 17 additions & 0 deletions pkg/oam/trait/manualscaler/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
Copyright 2019 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 manualscaler
50 changes: 50 additions & 0 deletions pkg/oam/trait/manualscaler/manualscaler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
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 manualscaler

import (
"context"

"github.com/pkg/errors"
appsv1 "k8s.io/api/apps/v1"
"k8s.io/apimachinery/pkg/runtime"

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

oamv1alpha2 "github.com/crossplane/crossplane/apis/oam/v1alpha2"
)

const (
errNotDeployment = "object to be modified is not a deployment"
errNotManualScalerTrait = "trait is not a manual scaler"
)

// DeploymentModifier modifies the replica count of a Deployment.
func DeploymentModifier(ctx context.Context, obj runtime.Object, t resource.Trait) error {
d, ok := obj.(*appsv1.Deployment)
if !ok {
return errors.New(errNotDeployment)
}

ms, ok := t.(*oamv1alpha2.ManualScalerTrait)
if !ok {
return errors.New(errNotManualScalerTrait)
}
d.Spec.Replicas = &ms.Spec.ReplicaCount

return nil
}
99 changes: 99 additions & 0 deletions pkg/oam/trait/manualscaler/manualscaler_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/*
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 manualscaler

import (
"context"
"testing"

"github.com/google/go-cmp/cmp"
"github.com/pkg/errors"
appsv1 "k8s.io/api/apps/v1"
"k8s.io/apimachinery/pkg/runtime"

"github.com/crossplane/crossplane-runtime/pkg/reconciler/oam/trait"
"github.com/crossplane/crossplane-runtime/pkg/resource"
"github.com/crossplane/crossplane-runtime/pkg/resource/fake"
"github.com/crossplane/crossplane-runtime/pkg/test"

oamv1alpha2 "github.com/crossplane/crossplane/apis/oam/v1alpha2"
)

var (
startingReplicas int32 = 2
)

var _ trait.Modifier = trait.ModifyFn(DeploymentModifier)

func TestDeploymentModifier(t *testing.T) {
type args struct {
o runtime.Object
t resource.Trait
}

type want struct {
err error
}

cases := map[string]struct {
reason string
args args
want want
}{
"ErrorObjectNotDeployment": {
reason: "Object passed to modifier that is not a Deployment should return error.",
args: args{
o: &appsv1.DaemonSet{},
},
want: want{err: errors.New(errNotDeployment)},
},
"ErrorTraitNotManualScaler": {
reason: "Trait passed to modifier that is not a ManualScalerTrait should return error.",
args: args{
o: &appsv1.Deployment{},
t: &fake.Trait{},
},
want: want{err: errors.New(errNotManualScalerTrait)},
},
"Success": {
reason: "A Deployment should have its replicas field changed on successful modification.",
args: args{
o: &appsv1.Deployment{
Spec: appsv1.DeploymentSpec{
Replicas: &startingReplicas,
},
},
t: &oamv1alpha2.ManualScalerTrait{
Spec: oamv1alpha2.ManualScalerTraitSpec{
ReplicaCount: 3,
},
},
},
want: want{},
},
}

for name, tc := range cases {
t.Run(name, func(t *testing.T) {
err := DeploymentModifier(context.Background(), tc.args.o, tc.args.t)

if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
t.Errorf("\nReason: %s\nDeploymentModifier(...): -want error, +got error:\n%s", tc.reason, diff)
}
})
}
}
75 changes: 75 additions & 0 deletions pkg/oam/trait/modify.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
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 trait

import (
"context"
"encoding/json"
"reflect"

"github.com/pkg/errors"
appsv1 "k8s.io/api/apps/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"

"github.com/crossplane/crossplane-runtime/pkg/reconciler/oam/trait"
"github.com/crossplane/crossplane-runtime/pkg/resource"

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

const (
errNotKubeApp = "object passed to KubernetesApplication accessor is not KubernetesApplication"
errNoDeploymentForTrait = "no deployment found for trait in KubernetesApplication"
)

var (
deploymentKind = reflect.TypeOf(appsv1.Deployment{}).Name()
)

// 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 {
return errors.New(errNotKubeApp)
}

for i, r := range a.Spec.ResourceTemplates {
template := &unstructured.Unstructured{}
if err := json.Unmarshal(r.Spec.Template.Raw, template); err != nil {
return err
}
if template.GroupVersionKind().Kind == deploymentKind {
d := &appsv1.Deployment{}
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(template.UnstructuredContent(), d); err != nil {
return err
}
if err := m(ctx, d, t); err != nil {
return err
}
deployment, err := json.Marshal(d)
if err != nil {
return err
}
a.Spec.ResourceTemplates[i].Spec.Template = runtime.RawExtension{Raw: deployment}
return nil
}
}

return errors.New(errNoDeploymentForTrait)
}
117 changes: 117 additions & 0 deletions pkg/oam/trait/modify_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/*
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 trait

import (
"context"
"testing"

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

runtimev1alpha1 "github.com/crossplane/crossplane-runtime/apis/core/v1alpha1"
"github.com/crossplane/crossplane-runtime/pkg/reconciler/oam/trait"
"github.com/crossplane/crossplane-runtime/pkg/resource"
"github.com/crossplane/crossplane-runtime/pkg/resource/fake"
"github.com/crossplane/crossplane-runtime/pkg/test"

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

var (
workloadName = "test-workload"
)

var _ trait.ModifyAccessor = DeploymentFromKubeAppAccessor

func TestDeploymentFromKubeAppAccessor(t *testing.T) {
type args struct {
o runtime.Object
t resource.Trait
m trait.ModifyFn
}

type want struct {
err error
}

cases := map[string]struct {
reason string
args args
want want
}{
"ErrorObjectIsNotKubeApp": {
reason: "Object passed to accessor that is not a KubernetesApplication should return error.",
args: args{
o: &workloadv1alpha1.KubernetesApplicationResource{},
},
want: want{err: errors.New(errNotKubeApp)},
},
"ErrorNoMatchingDeployment": {
reason: "Object passed to accessor that is not a KubernetesApplication should return error.",
args: args{
o: &workloadv1alpha1.KubernetesApplication{},
t: &fake.Trait{},
},
want: want{err: errors.New(errNoDeploymentForTrait)},
},
"SuccessfulNoopModifier": {
reason: "KubernetesApplication has matching Deployment and is modified successfully.",
args: args{
o: &workloadv1alpha1.KubernetesApplication{
ObjectMeta: metav1.ObjectMeta{},
Spec: workloadv1alpha1.KubernetesApplicationSpec{
ResourceTemplates: []workloadv1alpha1.KubernetesApplicationResourceTemplate{
{
ObjectMeta: metav1.ObjectMeta{
Name: workloadName,
},
Spec: workloadv1alpha1.KubernetesApplicationResourceSpec{
Template: runtime.RawExtension{Raw: []byte(`{
"kind":"Deployment",
"apiVersion":"apps/v1"
}`)},
},
},
},
},
},
t: &fake.Trait{
WorkloadReferencer: fake.WorkloadReferencer{
Ref: runtimev1alpha1.TypedReference{
Name: workloadName,
},
},
},
m: trait.NoopModifier,
},
want: want{},
},
}

for name, tc := range cases {
t.Run(name, func(t *testing.T) {
err := DeploymentFromKubeAppAccessor(context.Background(), tc.args.o, tc.args.t, tc.args.m)

if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
t.Errorf("\nReason: %s\nDeploymentFromKubeAppAccessor(...): -want error, +got error:\n%s", tc.reason, diff)
}
})
}
}
Loading

0 comments on commit af2a48c

Please sign in to comment.