From 8fb17882fdcda397d29c6adac8c148782c6c7e50 Mon Sep 17 00:00:00 2001 From: Shubham Minglani Date: Mon, 27 Mar 2017 20:52:36 +0530 Subject: [PATCH] implement labels --- docs/file-reference.md | 11 +++ examples/wordpress/storage_labels.yml | 37 ++++++++ pkg/encoding/v1/encoding.go | 17 +++- pkg/encoding/v1/encoding_test.go | 87 +++++++++++++++++- pkg/object/object.go | 11 +++ pkg/object/object_test.go | 44 +++++++++ pkg/transform/kubernetes/kubernetes.go | 48 +++++++--- pkg/util/util.go | 13 +++ pkg/util/util_test.go | 121 +++++++++++++++++++++++++ 9 files changed, 372 insertions(+), 17 deletions(-) create mode 100644 examples/wordpress/storage_labels.yml diff --git a/docs/file-reference.md b/docs/file-reference.md index 839f74d8..eedacae6 100644 --- a/docs/file-reference.md +++ b/docs/file-reference.md @@ -8,6 +8,8 @@ version: "0.1-dev" services: - name: foobar replicas: 3 + labels: + foo_label: bar_label containers: - image: foo/bar:tag env: @@ -57,6 +59,8 @@ Each service has name and list of the containers, while `replicas` can be option # name: foo replicas: 4 + labels: + foo_label: bar_label containers: - emptyDirVolumes: @@ -77,6 +81,13 @@ Name of the service. Number of desired pods of this particluar service. This is an optional field. The valid value can only be a positive number. +#### labels +| type | required | +|---------|----------| +| map with string keys and string values | no | + +Desired labels to be applied to the resulting Kubernetes objects from the service. + ### containers | type | required | |-----------------------------------------|----------| diff --git a/examples/wordpress/storage_labels.yml b/examples/wordpress/storage_labels.yml new file mode 100644 index 00000000..4aa7a451 --- /dev/null +++ b/examples/wordpress/storage_labels.yml @@ -0,0 +1,37 @@ +version: '0.1-dev' + +services: +- name: database + labels: + app: db + containers: + - image: mariadb:10 + env: + - MYSQL_ROOT_PASSWORD=rootpasswd + - MYSQL_DATABASE=wordpress + - MYSQL_USER=wordpress + - MYSQL_PASSWORD=wordpress + ports: + - port: 3306 + mounts: + - volumeName: database + mountPath: /var/lib/mysql + +- name: web + labels: + app: web + containers: + - image: wordpress:4 + env: + - WORDPRESS_DB_HOST=database:3306 + - WORDPRESS_DB_PASSWORD=wordpress + - WORDPRESS_DB_USER=wordpress + - WORDPRESS_DB_NAME=wordpress + ports: + - port: 80 + type: external + +volumes: +- name: database + size: 100Mi + accessMode: ReadWriteOnce diff --git a/pkg/encoding/v1/encoding.go b/pkg/encoding/v1/encoding.go index 186e214f..85dd9709 100644 --- a/pkg/encoding/v1/encoding.go +++ b/pkg/encoding/v1/encoding.go @@ -175,6 +175,19 @@ func (raw *EnvVariable) UnmarshalYAML(unmarshal func(interface{}) error) error { return nil } +type Labels object.Labels + +func (lb *Labels) UnmarshalYAML(unmarshal func(interface{}) error) error { + labelMap := make(map[string]string) + if err := unmarshal(&labelMap); err != nil { + return err + } + + *lb = Labels(labelMap) + + return nil +} + type ImageRef string // FIXME: implement ImageRef unmarshalling @@ -264,6 +277,7 @@ type Service struct { Containers []Container `yaml:"containers"` Replicas *int32 `yaml:"replicas,omitempty"` EmptyDirVolumes []EmptyDirVolume `yaml:"emptyDirVolumes,omitempty"` + Labels Labels `yaml:"labels,omitempty"` } func (s *Service) UnmarshalYAML(unmarshal func(interface{}) error) error { @@ -389,7 +403,8 @@ func (d *Decoder) Decode(data []byte) (*object.OpenCompose, error) { // convert services for _, s := range v1.Services { os := object.Service{ - Name: string(s.Name), + Name: string(s.Name), + Labels: object.Labels(s.Labels), } os.Replicas = s.Replicas diff --git a/pkg/encoding/v1/encoding_test.go b/pkg/encoding/v1/encoding_test.go index a878764d..91a12e2c 100644 --- a/pkg/encoding/v1/encoding_test.go +++ b/pkg/encoding/v1/encoding_test.go @@ -336,10 +336,7 @@ readOnly: true &Mount{ VolumeName: "test-volume", ReadOnly: goutil.BoolAddr(true), - }, - }, - } - + }}} for _, test := range tests { t.Run(test.Name, func(t *testing.T) { var mount Mount @@ -402,6 +399,52 @@ excess: field } } +func TestLabels_UnmarshalYAML(t *testing.T) { + tests := []struct { + name string + Succeed bool + RawLabels string + Labels *Labels + }{ + { + "Providing valid label strings", + true, + ` +key1: value1 +key2: value2 +key3: +key4: value4 +`, + &Labels{ + "key1": "value1", + "key2": "value2", + "key3": "", + "key4": "value4", + }, + }, + } + for _, tt := range tests { + var labels Labels + err := yaml.Unmarshal([]byte(tt.RawLabels), &labels) + if err != nil { + if tt.Succeed { + t.Errorf("Failed to unmarshal %#v; error %#v", tt.RawLabels, err) + } + continue + } + + if !tt.Succeed { + t.Errorf("Expected %#v to fail!", tt.RawLabels) + continue + } + + if !reflect.DeepEqual(labels, *tt.Labels) { + t.Errorf("Expected %#v, got %#v", *tt.Labels, labels) + continue + } + } +} + func TestService_UnmarshalYAML(t *testing.T) { tests := []struct { Name string @@ -979,6 +1022,42 @@ volumes: `, nil, }, + { + true, + ` +version: 0.1-dev +services: +- name: helloworld + replicas: 2 + containers: + - image: tomaskral/nonroot-nginx + labels: + key1: value1 + key2: value2 + key3: + key4: value4 +`, + &object.OpenCompose{ + Version: Version, + Services: []object.Service{ + { + Name: "helloworld", + Replicas: goutil.Int32Addr(2), + Containers: []object.Container{ + { + Image: "tomaskral/nonroot-nginx", + }, + }, + Labels: object.Labels{ + "key1": "value1", + "key2": "value2", + "key3": "", + "key4": "value4", + }, + }, + }, + }, + }, } for _, tt := range tests { diff --git a/pkg/object/object.go b/pkg/object/object.go index b03d83cb..c18a3932 100644 --- a/pkg/object/object.go +++ b/pkg/object/object.go @@ -42,6 +42,8 @@ type Mount struct { ReadOnly bool } +type Labels map[string]string + type Container struct { Image string Environment []EnvVariable @@ -58,6 +60,7 @@ type Service struct { Containers []Container Replicas *int32 EmptyDirVolumes []EmptyDirVolume + Labels Labels } type Volume struct { @@ -163,6 +166,14 @@ func (s *Service) validate() error { } } + // validate label values + for _, v := range s.Labels { + errString := validation.IsValidLabelValue(v) + if errString != nil { + return fmt.Errorf("Invalid label value: %v", errString) + } + } + return nil } diff --git a/pkg/object/object_test.go b/pkg/object/object_test.go index 8c67b580..fd9f3e38 100644 --- a/pkg/object/object_test.go +++ b/pkg/object/object_test.go @@ -385,6 +385,50 @@ func TestOpenCompose_Validate(t *testing.T) { }, }, }, + + { + "Valid labels", + true, + &OpenCompose{ + Version: Version, + Services: []Service{ + { + Name: name, + Containers: []Container{ + { + Image: image, + }, + }, + Labels: Labels{ + "key1": "value1", + "key2": "value2", + }, + }, + }, + }, + }, + + { + "Invalid label values", + false, + &OpenCompose{ + Version: Version, + Services: []Service{ + { + Name: name, + Containers: []Container{ + { + Image: image, + }, + }, + Labels: Labels{ + "key1": "garbage^value", + "key2": "value2", + }, + }, + }, + }, + }, } for _, test := range tests { diff --git a/pkg/transform/kubernetes/kubernetes.go b/pkg/transform/kubernetes/kubernetes.go index a84f3df2..c240462c 100644 --- a/pkg/transform/kubernetes/kubernetes.go +++ b/pkg/transform/kubernetes/kubernetes.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/redhat-developer/opencompose/pkg/object" + "github.com/redhat-developer/opencompose/pkg/util" _ "k8s.io/client-go/pkg/api/install" "k8s.io/client-go/pkg/api/resource" api_v1 "k8s.io/client-go/pkg/api/v1" @@ -20,12 +21,18 @@ func (t *Transformer) CreateServices(o *object.Service) ([]runtime.Object, error result := []runtime.Object{} Service := func() *api_v1.Service { + serviceLabels := map[string]string(o.Labels) return &api_v1.Service{ ObjectMeta: api_v1.ObjectMeta{ Name: o.Name, - Labels: map[string]string{ - "service": o.Name, - }, + Labels: *util.MergeMaps( + // The map containing `"service": o.Name` should always be + // passed later to avoid being overridden by util.MergeMaps() + &serviceLabels, + &map[string]string{ + "service": o.Name, + }, + ), }, Spec: api_v1.ServiceSpec{ Selector: map[string]string{ @@ -81,13 +88,19 @@ func (t *Transformer) CreateServices(o *object.Service) ([]runtime.Object, error // Create k8s ingresses for OpenCompose service func (t *Transformer) CreateIngresses(o *object.Service) ([]runtime.Object, error) { result := []runtime.Object{} + serviceLabels := map[string]string(o.Labels) i := &ext_v1beta1.Ingress{ ObjectMeta: api_v1.ObjectMeta{ Name: o.Name, - Labels: map[string]string{ - "service": o.Name, - }, + Labels: *util.MergeMaps( + // The map containing `"service": o.Name` should always be + // passed later to avoid being overridden by util.MergeMaps() + &serviceLabels, + &map[string]string{ + "service": o.Name, + }, + ), }, } @@ -142,13 +155,19 @@ func (t *Transformer) CreateIngresses(o *object.Service) ([]runtime.Object, erro // Create k8s deployments for OpenCompose service func (t *Transformer) CreateDeployments(s *object.Service) ([]runtime.Object, error) { result := []runtime.Object{} + serviceLabels := map[string]string(s.Labels) d := &ext_v1beta1.Deployment{ ObjectMeta: api_v1.ObjectMeta{ Name: s.Name, - Labels: map[string]string{ - "service": s.Name, - }, + Labels: *util.MergeMaps( + // The map containing `"service": s.Name` should always be + // passed later to avoid being overridden by util.MergeMaps() + &serviceLabels, + &map[string]string{ + "service": s.Name, + }, + ), }, Spec: ext_v1beta1.DeploymentSpec{ Strategy: ext_v1beta1.DeploymentStrategy{ @@ -159,9 +178,14 @@ func (t *Transformer) CreateDeployments(s *object.Service) ([]runtime.Object, er }, Template: api_v1.PodTemplateSpec{ ObjectMeta: api_v1.ObjectMeta{ - Labels: map[string]string{ - "service": s.Name, - }, + Labels: *util.MergeMaps( + // The map containing `"service": s.Name` should always be + // passed later to avoid being overridden by util.MergeMaps() + &serviceLabels, + &map[string]string{ + "service": s.Name, + }, + ), }, Spec: api_v1.PodSpec{}, }, diff --git a/pkg/util/util.go b/pkg/util/util.go index dd0f69ba..6a2ad7a7 100644 --- a/pkg/util/util.go +++ b/pkg/util/util.go @@ -69,3 +69,16 @@ func FetchURLWithRetries(url string, attempts int, duration time.Duration) ([]by return data, err } + +// MergeMaps will merge the given maps, but it does not check for conflicts. +// In case of conflicting keys, the map that is provided later overrides the previous one. +// TODO: add to docs about use with caution bits +func MergeMaps(maps ...*map[string]string) *map[string]string { + mergedMap := make(map[string]string) + for _, m := range maps { + for k, v := range *m { + mergedMap[k] = v + } + } + return &mergedMap +} diff --git a/pkg/util/util_test.go b/pkg/util/util_test.go index 04a8f8bc..e78c9c6e 100644 --- a/pkg/util/util_test.go +++ b/pkg/util/util_test.go @@ -1,6 +1,7 @@ package util import ( + "reflect" "testing" "time" ) @@ -64,3 +65,123 @@ func TestFetchURLWithRetries(t *testing.T) { }) } } + +func TestMergeMaps(t *testing.T) { + no_map1 := map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + } + no_map2 := map[string]string{ + "key4": "value4", + "key5": "value5", + "key6": "value6", + } + no_map3 := map[string]string{ + "key7": "value7", + "key8": "value8", + "key9": "value9", + } + o_map1 := map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + } + o_map2 := map[string]string{ + "key4": "value4", + "key2": "value5", + "key1": "value6", + } + o_map3 := map[string]string{ + "key4": "value7", + "key5": "value8", + "key6": "value9", + } + no_map12 := map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + "key4": "value4", + "key5": "value5", + "key6": "value6", + } + no_map123 := map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + "key4": "value4", + "key5": "value5", + "key6": "value6", + "key7": "value7", + "key8": "value8", + "key9": "value9", + } + o_map12 := map[string]string{ + "key3": "value3", + "key4": "value4", + "key2": "value5", + "key1": "value6", + } + o_map123 := map[string]string{ + "key3": "value3", + "key2": "value5", + "key1": "value6", + "key4": "value7", + "key5": "value8", + "key6": "value9", + } + o_map231 := map[string]string{ + "key4": "value7", + "key5": "value8", + "key6": "value9", + "key1": "value1", + "key2": "value2", + "key3": "value3", + } + + tests := []struct { + name string + input []*map[string]string + output *map[string]string + }{ + { + "passing 1 map", + []*map[string]string{&no_map1}, + &no_map1, + }, + { + "merging 2 non overlapping maps", + []*map[string]string{&no_map1, &no_map2}, + &no_map12, + }, + { + "merging 3 non overlapping maps", + []*map[string]string{&no_map1, &no_map2, &no_map3}, + &no_map123, + }, + { + "merging 2 overlapping maps", + []*map[string]string{&o_map1, &o_map2}, + &o_map12, + }, + { + "merging 3 overlapping maps", + []*map[string]string{&o_map1, &o_map2, &o_map3}, + &o_map123, + }, + { + "merging 3 overlapping maps in a different order", + []*map[string]string{&o_map2, &o_map3, &o_map1}, + &o_map231, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mergedMap := MergeMaps(tt.input...) + if !reflect.DeepEqual(*tt.output, *mergedMap) { + t.Fatalf("The expected output - %v - is different than the resulting merged map - %v", *tt.output, *mergedMap) + } + }) + } +}