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

Commit

Permalink
implement ContainerConfigFile of ContainerizedWorkload
Browse files Browse the repository at this point in the history
add e2e-test

add unit test

Signed-off-by: roy wang <seiwy2010@gmail.com>
  • Loading branch information
captainroy-hy committed Nov 3, 2020
1 parent 2df672c commit 8702f4d
Show file tree
Hide file tree
Showing 8 changed files with 257 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,4 @@ spec:
- scopeRef:
apiVersion: core.oam.dev/v1alpha2
kind: HealthScope
name: example-health-scope
name: example-health-scope
9 changes: 8 additions & 1 deletion examples/containerized-workload/sample_component.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,17 @@ spec:
env:
- name: TEST_ENV
value: test
config:
- path: /test/configfile/config
value: test
- path: /test/secretconfig
fromSecret:
name: mysecret
key: password
parameters:
- name: instance-name
fieldPaths:
- metadata.name
- name: image
fieldPaths:
- spec.containers[0].image
- spec.containers[0].image
8 changes: 8 additions & 0 deletions examples/containerized-workload/sample_secret.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
apiVersion: v1
kind: Secret
metadata:
name: mysecret
type: Opaque
data:
username: YWRtaW4=
password: MWYyZDFlMmU2N2Rm
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ const (
errRenderWorkload = "cannot render workload"
errRenderService = "cannot render service"
errApplyDeployment = "cannot apply the deployment"
errApplyConfigMap = "cannot apply the configmap"
errApplyService = "cannot apply the service"
)

Expand Down Expand Up @@ -71,6 +72,7 @@ type Reconciler struct {
// +kubebuilder:rbac:groups=core.oam.dev,resources=containerizedworkloads/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=core,resources=services,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=core,resources=configmaps,verbs=get;list;watch;create;update;patch;delete
func (r *Reconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
ctx := context.Background()
log := r.log.WithValues("containerizedworkload", req.NamespacedName)
Expand Down Expand Up @@ -110,6 +112,25 @@ func (r *Reconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
fmt.Sprintf("Workload `%s` successfully server side patched a deployment `%s`",
workload.Name, deploy.Name)))

configMapApplyOpts := []client.PatchOption{client.ForceOwnership, client.FieldOwner(deploy.GetUID())}
configmaps, err := r.renderConfigMaps(ctx, &workload, deploy)
if err != nil {
log.Error(err, "Failed to render configmaps")
r.record.Event(eventObj, event.Warning(errRenderWorkload, err))
return util.ReconcileWaitResult,
util.PatchCondition(ctx, r, &workload, cpv1alpha1.ReconcileError(errors.Wrap(err, errRenderWorkload)))
}
for _, cm := range configmaps {
if err := r.Patch(ctx, cm, client.Apply, configMapApplyOpts...); err != nil {
log.Error(err, "Failed to apply a configmap")
r.record.Event(eventObj, event.Warning(errApplyConfigMap, err))
return util.ReconcileWaitResult,
util.PatchCondition(ctx, r, &workload, cpv1alpha1.ReconcileError(errors.Wrap(err, errApplyConfigMap)))
}
r.record.Event(eventObj, event.Normal("ConfigMap created",
fmt.Sprintf("Workload `%s` successfully server side patched a configmap `%s`",
workload.Name, cm.Name)))
}
// create a service for the workload
// TODO(rz): remove this after we have service trait
service, err := r.renderService(ctx, &workload, deploy)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,22 @@ func (r *Reconciler) renderService(ctx context.Context,
return service, nil
}

// create ConfigMaps for ContainerConfigFiles
func (r *Reconciler) renderConfigMaps(ctx context.Context,
workload *v1alpha2.ContainerizedWorkload, deploy *appsv1.Deployment) ([]*corev1.ConfigMap, error) {
configMaps, err := TranslateConfigMaps(ctx, workload)
if err != nil {
return nil, err
}
for _, cm := range configMaps {
// always set the controller reference so that we can watch this configmap and it will be deleted automatically
if err := ctrl.SetControllerReference(deploy, cm, r.Scheme); err != nil {
return nil, err
}
}
return configMaps, nil
}

// delete deployments/services that are not the same as the existing
// nolint:gocyclo
func (r *Reconciler) cleanupResources(ctx context.Context,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (

"github.com/crossplane/crossplane-runtime/pkg/test"
"github.com/google/go-cmp/cmp"
appsv1 "k8s.io/api/apps/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
Expand Down Expand Up @@ -62,7 +63,27 @@ func TestRenderDeployment(t *testing.T) {
}
dmAnnotation := cwAnnotation

w := containerizedWorkload(cwWithAnnotation(cwAnnotation), cwWithLabel(cwLabel))
configVolumeMountPath := "/test/path/config"
configValue := "testValue"
secretVolumeMountPath := "/test/path/secret"
secretKey, secretName := "testKey", "testName"
w := containerizedWorkload(cwWithAnnotation(cwAnnotation), cwWithLabel(cwLabel), cwWithContainer(
v1alpha2.Container{
ConfigFiles: []v1alpha2.ContainerConfigFile{
{
Path: secretVolumeMountPath,
FromSecret: &v1alpha2.SecretKeySelector{
Key: secretKey,
Name: secretName,
},
},
{
Path: configVolumeMountPath,
Value: &configValue,
},
},
},
))
deploy, err := r.renderDeployment(context.Background(), w)

if diff := cmp.Diff(nil, err, test.EquateErrors()); diff != "" {
Expand Down Expand Up @@ -91,4 +112,59 @@ func TestRenderDeployment(t *testing.T) {
t.Errorf("deplyment should have one owner reference pointing to the ContainerizedWorkload")
}

if diff := cmp.Diff(2, len(deploy.Spec.Template.Spec.Volumes)); diff != "" {
t.Errorf("\nReason: %s\ncontainerizedWorkloadTranslator(...): -want, +got:\n%s", "render volumes", diff)
}
secretVM := deploy.Spec.Template.Spec.Volumes[0]
if diff := cmp.Diff(secretName, secretVM.Secret.SecretName); diff != "" {
t.Errorf("\nReason: %s\ncontainerizedWorkloadTranslator(...): -want, +got:\n%s", "render volumes", diff)
}
if diff := cmp.Diff(1, len(deploy.Spec.Template.Spec.Containers)); diff != "" {
t.Errorf("\nReason: %s\ncontainerizedWorkloadTranslator(...): -want, +got:\n%s", "render deplyment podTemplate", diff)
}
c := deploy.Spec.Template.Spec.Containers[0]
if diff := cmp.Diff(2, len(c.VolumeMounts)); diff != "" {
t.Errorf("\nReason: %s\ncontainerizedWorkloadTranslator(...): -want, +got:\n%s", "render volume mount", diff)
}

}

func TestRenderConfigMaps(t *testing.T) {
var scheme = runtime.NewScheme()
_ = clientgoscheme.AddToScheme(scheme)
_ = core.AddToScheme(scheme)

r := Reconciler{
Client: nil,
log: ctrl.Log.WithName("ContainerizedWorkload"),
record: nil,
Scheme: scheme,
}
testValue := "test value"
testContainerName := "testContainerName"
testContainerConfigFile := v1alpha2.ContainerConfigFile{
Path: "/test/path/configmap",
Value: &testValue,
}
w := containerizedWorkload(cwWithContainer(v1alpha2.Container{
Name: testContainerName,
ConfigFiles: []v1alpha2.ContainerConfigFile{testContainerConfigFile},
}))

configMaps, err := r.renderConfigMaps(context.Background(), w, &appsv1.Deployment{})
if diff := cmp.Diff(nil, err, test.EquateErrors()); diff != "" {
t.Errorf("\nReason: %s\nrenderConfigMaps(...): -want error, +got error:\n%s", "translate into ConfigMaps", diff)
}
if diff := cmp.Diff(1, len(configMaps)); diff != "" {
t.Errorf("\nReason: %s\nrenderConfigMaps(...): -want error, +got error:\n%s", "translate into ConfigMaps", diff)
}
cm := configMaps[0]
expectName, _ := generateConfigMapName(testContainerConfigFile, workloadName, testContainerName)
if diff := cmp.Diff(expectName, cm.Name); diff != "" {
t.Errorf("\nReason: %s\ngenerateConfigMapName(...): -want error, +got error:\n%s", "translate into ConfigMaps", diff)
}
if diff := cmp.Diff(cm.Data["configmap"], testValue); diff != "" {
t.Errorf("\nReason: %s\nrenderConfigMaps(...): -want error, +got error:\n%s", "translate into ConfigMaps", diff)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ package containerizedworkload
import (
"context"
"errors"
"fmt"
"hash/fnv"
"path"
"reflect"

appsv1 "k8s.io/api/apps/v1"
Expand All @@ -36,6 +39,8 @@ var (
deploymentAPIVersion = appsv1.SchemeGroupVersion.String()
serviceKind = reflect.TypeOf(corev1.Service{}).Name()
serviceAPIVersion = corev1.SchemeGroupVersion.String()
configMapKind = reflect.TypeOf(corev1.ConfigMap{}).Name()
configMapAPIVersion = corev1.SchemeGroupVersion.String()
)

// Reconcile error strings.
Expand Down Expand Up @@ -253,6 +258,12 @@ func TranslateContainerWorkload(ctx context.Context, w oam.Workload) ([]oam.Obje
}
}

for _, c := range container.ConfigFiles {
v, vm := translateConfigFileToVolume(c, w.GetName(), container.Name)
kubernetesContainer.VolumeMounts = append(kubernetesContainer.VolumeMounts, vm)
d.Spec.Template.Spec.Volumes = append(d.Spec.Template.Spec.Volumes, v)
}

d.Spec.Template.Spec.Containers = append(d.Spec.Template.Spec.Containers, kubernetesContainer)
}

Expand All @@ -264,6 +275,100 @@ func TranslateContainerWorkload(ctx context.Context, w oam.Workload) ([]oam.Obje
return []oam.Object{d}, nil
}

func translateConfigFileToVolume(cf v1alpha2.ContainerConfigFile, wlName, containerName string) (v corev1.Volume, vm corev1.VolumeMount) {
mountPath, _ := path.Split(cf.Path)
// translate into ConfigMap Volume
if cf.Value != nil {
name, _ := generateConfigMapName(cf, wlName, containerName)
v = corev1.Volume{
Name: name,
VolumeSource: corev1.VolumeSource{
ConfigMap: &corev1.ConfigMapVolumeSource{
LocalObjectReference: corev1.LocalObjectReference{Name: name},
},
},
}
vm = corev1.VolumeMount{
MountPath: mountPath,
Name: name,
}
return v, vm
}

// translate into Secret Volume
secretName := cf.FromSecret.Name
itemKey := cf.FromSecret.Key
v = corev1.Volume{
Name: secretName,
VolumeSource: corev1.VolumeSource{
Secret: &corev1.SecretVolumeSource{
SecretName: secretName,
Items: []corev1.KeyToPath{
{
Key: itemKey,
// OAM v1alpha2 SecretKeySelector doen't provide Path field
// just use itemKey as relative Path
Path: itemKey,
},
},
},
},
}
vm = corev1.VolumeMount{
MountPath: cf.Path,
Name: secretName,
}
return v, vm
}

func generateConfigMapName(cf v1alpha2.ContainerConfigFile, wlName, containerName string) (string, error) {
h := fnv.New32a()
_, err := h.Write([]byte(cf.Path))
if err != nil {
return "", err
}
return fmt.Sprintf("%s-%s-%d", wlName, containerName, h.Sum32()), nil
}

// TranslateConfigMaps translate non-secret ContainerConfigFile into ConfigMaps
func TranslateConfigMaps(ctx context.Context, w oam.Object) ([]*corev1.ConfigMap, error) {
cw, ok := w.(*v1alpha2.ContainerizedWorkload)
if !ok {
return nil, errors.New(errNotContainerizedWorkload)
}

newConfigMaps := []*corev1.ConfigMap{}
for _, c := range cw.Spec.Containers {
for _, cf := range c.ConfigFiles {
if cf.Value == nil {
continue
}
_, key := path.Split(cf.Path)
cmName, err := generateConfigMapName(cf, cw.GetName(), c.Name)
if err != nil {
return nil, err
}
cm := &corev1.ConfigMap{
TypeMeta: metav1.TypeMeta{
Kind: configMapKind,
APIVersion: configMapAPIVersion,
},
ObjectMeta: metav1.ObjectMeta{
Name: cmName,
Namespace: cw.GetNamespace(),
},
Data: map[string]string{
key: *cf.Value,
},
}
// pass through label and annotation from the workload to the configmap
util.PassLabelAndAnnotation(w, cm)
newConfigMaps = append(newConfigMaps, cm)
}
}
return newConfigMaps, nil
}

// ServiceInjector adds a Service object for the first Port on the first
// Container for the first Deployment observed in a workload translation.
func ServiceInjector(ctx context.Context, w oam.Workload, objs []oam.Object) ([]oam.Object, error) {
Expand Down
21 changes: 21 additions & 0 deletions test/e2e-test/containerized_workload_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package controllers_test

import (
"context"
"fmt"
"time"

. "github.com/onsi/ginkgo"
Expand Down Expand Up @@ -93,6 +94,7 @@ var _ = Describe("ContainerizedWorkload", func() {
},
}
// create a workload CR
configFileValue := "testValue"
wl = v1alpha2.ContainerizedWorkload{
ObjectMeta: metav1.ObjectMeta{
Namespace: namespace,
Expand All @@ -109,6 +111,12 @@ var _ = Describe("ContainerizedWorkload", func() {
Port: 80,
},
},
ConfigFiles: []v1alpha2.ContainerConfigFile{
{
Path: "/test/path/config",
Value: &configFileValue,
},
},
},
},
},
Expand Down Expand Up @@ -238,6 +246,19 @@ var _ = Describe("ContainerizedWorkload", func() {
HaveKey(oam.LabelAppName),
HaveKey(oam.LabelOAMResourceType)))

By("Checking ConfigMap is created")
cmObjectKey := client.ObjectKey{
Namespace: namespace,
Name: fmt.Sprintf("%s-%s-3972676475", workloadInstanceName, "wordpress"),
}
configMap := &corev1.ConfigMap{}
logf.Log.Info("Checking on configMap", "Key", cmObjectKey)
Eventually(
func() error {
return k8sClient.Get(ctx, cmObjectKey, configMap)
},
time.Second*15, time.Millisecond*500).Should(BeNil())

By("Checking deployment is created")
objectKey := client.ObjectKey{
Name: workloadInstanceName,
Expand Down

0 comments on commit 8702f4d

Please sign in to comment.