diff --git a/pkg/provisioner/block/block.go b/pkg/provisioner/block/block.go index 4a8f1998e..f6ddca735 100644 --- a/pkg/provisioner/block/block.go +++ b/pkg/provisioner/block/block.go @@ -38,8 +38,11 @@ import ( ) const ( - ociVolumeID = "ociVolumeID" - ociVolumeBackupID = "volume.beta.kubernetes.io/oci-volume-source" + ociVolumeID = "ociVolumeID" + ociVolumeBackupID = "volume.beta.kubernetes.io/oci-volume-source" + ociTagAnnotation = "oraclecloud.com/additional-tags" + + defaultTagsEnvVar = "OCI_DEFAULT_TAGS" volumePrefixEnvVarName = "OCI_VOLUME_NAME_PREFIX" fsType = "fsType" ) @@ -113,6 +116,14 @@ func (block *blockProvisioner) Provision(options controller.VolumeOptions, ad *i SizeInMBs: common.Int(volSizeMB), } + definedTags, freeformTags, err := getTags(options.PVC.Annotations) + if err != nil { + return nil, err + } + + volumeDetails.DefinedTags = definedTags + volumeDetails.FreeformTags = freeformTags + if value, ok := options.PVC.Annotations[ociVolumeBackupID]; ok { glog.Infof("Creating volume from backup ID %s", value) volumeDetails.SourceDetails = &core.VolumeSourceFromVolumeBackupDetails{Id: &value} @@ -172,6 +183,72 @@ func (block *blockProvisioner) Provision(options controller.VolumeOptions, ad *i return pv, nil } +func getTags(annotations map[string]string) (defined map[string]map[string]interface{}, freeform map[string]string, err error) { + defaultDefinedTags, defaultFreeformTags, err := parseTags(os.Getenv(defaultTagsEnvVar)) + if err != nil { + return nil, nil, err + } + + definedTags, freeformTags, err := parseTags(annotations[ociTagAnnotation]) + if err != nil { + return nil, nil, err + } + + // merge annotation tags with default tags + for namespace, tags := range definedTags { + if _, ok := defaultDefinedTags[namespace]; !ok { + defaultDefinedTags[namespace] = map[string]interface{}{} + } + + for tag, value := range tags { + defaultDefinedTags[namespace][tag] = value + } + } + + for tag, value := range freeformTags { + defaultFreeformTags[tag] = value + } + + return defaultDefinedTags, defaultFreeformTags, nil +} + +func parseTags(tagStr string) (defined map[string]map[string]interface{}, freeform map[string]string, err error) { + + defined = map[string]map[string]interface{}{} + freeform = map[string]string{} + + if tagStr == "" { + return + } + + for _, tag := range strings.Split(tagStr, ",") { + parts := strings.Split(tag, "=") + if len(parts) != 2 { + return nil, nil, fmt.Errorf("tag format must follow (.)=: %q", tag) + } + + key, value := parts[0], parts[1] + + keyParts := strings.Split(key, ".") + if len(keyParts) == 1 { + freeform[key] = value + } else if len(keyParts) == 2 { + namespace, key := keyParts[0], keyParts[1] + namespaceTags, ok := defined[namespace] + if !ok { + namespaceTags = map[string]interface{}{} + defined[namespace] = namespaceTags + } + + namespaceTags[key] = value + } else { + return nil, nil, fmt.Errorf("tag format must follow (.)=: %q", tag) + } + } + + return +} + // Delete destroys a OCI volume created by Provision func (block *blockProvisioner) Delete(volume *v1.PersistentVolume) error { volID, ok := volume.Annotations[ociVolumeID] diff --git a/pkg/provisioner/block/block_test.go b/pkg/provisioner/block/block_test.go index 84172f196..f9040c8c0 100644 --- a/pkg/provisioner/block/block_test.go +++ b/pkg/provisioner/block/block_test.go @@ -17,6 +17,8 @@ package block import ( "context" "fmt" + "os" + "reflect" "testing" "time" @@ -181,6 +183,102 @@ func TestVolumeRoundingLogic(t *testing.T) { } } +func TestGetTags(t *testing.T) { + testCases := map[string]struct { + setup func() + annotations map[string]string + expectedDefinedTags map[string]map[string]interface{} + expectedFreeformTags map[string]string + }{ + "no default or annotation tags": { + setup: func() { + os.Setenv(defaultTagsEnvVar, "") + }, + annotations: map[string]string{}, + expectedDefinedTags: map[string]map[string]interface{}{}, + expectedFreeformTags: map[string]string{}, + }, + "valid tags only default": { + setup: func() { + os.Setenv(defaultTagsEnvVar, "defaultnamespace.default=test,default=test") + }, + annotations: map[string]string{}, + expectedDefinedTags: map[string]map[string]interface{}{ + "defaultnamespace": map[string]interface{}{ + "default": "test", + }, + }, + expectedFreeformTags: map[string]string{ + "default": "test", + }, + }, + "valid tags with default": { + setup: func() { + os.Setenv(defaultTagsEnvVar, "namespace1.default=test,default=test") + }, + annotations: map[string]string{ + ociTagAnnotation: "namespace1.test=foo,namespace2.test=bar,bar=baz,namespace1.test2=bar,foo=bar", + }, + expectedDefinedTags: map[string]map[string]interface{}{ + "namespace1": map[string]interface{}{ + "test": "foo", + "test2": "bar", + "default": "test", + }, + "namespace2": map[string]interface{}{ + "test": "bar", + }, + }, + expectedFreeformTags: map[string]string{ + "bar": "baz", + "foo": "bar", + "default": "test", + }, + }, + "override defaults with annotation": { + setup: func() { + os.Setenv(defaultTagsEnvVar, "namespace1.default=test,default=test") + }, + annotations: map[string]string{ + ociTagAnnotation: "namespace1.default=foo,default=bar", + }, + expectedDefinedTags: map[string]map[string]interface{}{ + "namespace1": map[string]interface{}{ + "default": "foo", + }, + }, + expectedFreeformTags: map[string]string{ + "default": "bar", + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + defer func() { + os.Setenv(defaultTagsEnvVar, "") + }() + + if tc.setup != nil { + tc.setup() + } + + defined, freeform, err := getTags(tc.annotations) + if err != nil { + t.Fatal(err) + } + + if !reflect.DeepEqual(defined, tc.expectedDefinedTags) { + t.Errorf("defined tags not equal: got %v ; wanted %v", defined, tc.expectedDefinedTags) + } + if !reflect.DeepEqual(freeform, tc.expectedFreeformTags) { + t.Errorf("freeform tags not equal: got %v ; wanted %v", freeform, tc.expectedFreeformTags) + } + }) + + } +} + func createPVC(size string) *v1.PersistentVolumeClaim { return &v1.PersistentVolumeClaim{ ObjectMeta: metav1.ObjectMeta{},