Skip to content

Commit 0fb57cb

Browse files
VihasMakwanadjaglowskiandrzej-stencel
authored
[filestorage]: Provide an option to the user to create a directory (#34985)
**Description:** This PR introduces a new option that will create a directory for the user if it doesn't already exist. **Link to tracking Issue:** #34939 **Testing:** Added --------- Co-authored-by: Daniel Jaglowski <[email protected]> Co-authored-by: Andrzej Stencel <[email protected]>
1 parent 88e72f0 commit 0fb57cb

File tree

8 files changed

+270
-13
lines changed

8 files changed

+270
-13
lines changed
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Use this changelog template to create an entry for release notes.
2+
3+
# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
4+
change_type: enhancement
5+
6+
# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver)
7+
component: file_storage
8+
9+
# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
10+
note: provide a new option to the user to create a directory on start
11+
12+
# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists.
13+
issues: [34939]

extension/storage/filestorage/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ The default timeout is `1s`.
2424

2525
`fsync` when set, will force the database to perform an fsync after each write. This helps to ensure database integrity if there is an interruption to the database process, but at the cost of performance. See [DB.NoSync](https://pkg.go.dev/go.etcd.io/bbolt#DB) for more information.
2626

27+
`create_directory` when set, will create the data storage and compaction directory if it does not already exist. The directory will be created with `0750 (rwxr-x--)` permissions, by default. Use `directory_permissions` to customize directory creation permissions.
28+
29+
2730
## Compaction
2831
`compaction` defines how and when files should be compacted. There are two modes of compaction available (both of which can be set concurrently):
2932
- `compaction.on_start` (default: false), which happens when collector starts

extension/storage/filestorage/config.go

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,13 @@ import (
88
"fmt"
99
"io/fs"
1010
"os"
11+
"strconv"
1112
"time"
1213
)
1314

15+
var errInvalidOctal = errors.New("directory_permissions value must be a valid octal representation")
16+
var errInvalidPermissionBits = errors.New("directory_permissions contain invalid bits for file access")
17+
1418
// Config defines configuration for file storage extension.
1519
type Config struct {
1620
Directory string `mapstructure:"directory,omitempty"`
@@ -20,6 +24,11 @@ type Config struct {
2024

2125
// FSync specifies that fsync should be called after each database write
2226
FSync bool `mapstructure:"fsync,omitempty"`
27+
28+
// CreateDirectory specifies that the directory should be created automatically by the extension on start
29+
CreateDirectory bool `mapstructure:"create_directory,omitempty"`
30+
DirectoryPermissions string `mapstructure:"directory_permissions,omitempty"`
31+
directoryPermissionsParsed int64 `mapstructure:"-,omitempty"`
2332
}
2433

2534
// CompactionConfig defines configuration for optional file storage compaction.
@@ -59,18 +68,16 @@ func (cfg *Config) Validate() error {
5968
dirs = []string{cfg.Directory}
6069
}
6170
for _, dir := range dirs {
62-
info, err := os.Stat(dir)
63-
if err != nil {
64-
if os.IsNotExist(err) {
65-
return fmt.Errorf("directory must exist: %w", err)
71+
if info, err := os.Stat(dir); err != nil {
72+
if !cfg.CreateDirectory && os.IsNotExist(err) {
73+
return fmt.Errorf("directory must exist: %w. You can enable the create_directory option to automatically create it", err)
6674
}
6775

6876
fsErr := &fs.PathError{}
69-
if errors.As(err, &fsErr) {
77+
if errors.As(err, &fsErr) && !os.IsNotExist(err) {
7078
return fmt.Errorf("problem accessing configured directory: %s, err: %w", dir, fsErr)
7179
}
72-
}
73-
if !info.IsDir() {
80+
} else if !info.IsDir() {
7481
return fmt.Errorf("%s is not a directory", dir)
7582
}
7683
}
@@ -83,5 +90,15 @@ func (cfg *Config) Validate() error {
8390
return errors.New("compaction check interval must be positive when rebound compaction is set")
8491
}
8592

93+
if cfg.CreateDirectory {
94+
permissions, err := strconv.ParseInt(cfg.DirectoryPermissions, 8, 32)
95+
if err != nil {
96+
return errInvalidOctal
97+
} else if permissions&int64(os.ModePerm) != permissions {
98+
return errInvalidPermissionBits
99+
}
100+
cfg.directoryPermissionsParsed = permissions
101+
}
102+
86103
return nil
87104
}

extension/storage/filestorage/config_test.go

Lines changed: 115 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@ import (
1010
"testing"
1111
"time"
1212

13+
"github.com/google/uuid"
1314
"github.com/stretchr/testify/assert"
1415
"github.com/stretchr/testify/require"
1516
"go.opentelemetry.io/collector/component"
1617
"go.opentelemetry.io/collector/confmap/confmaptest"
18+
"go.opentelemetry.io/collector/extension"
1719

1820
"github.com/open-telemetry/opentelemetry-collector-contrib/extension/storage/filestorage/internal/metadata"
1921
)
@@ -47,8 +49,10 @@ func TestLoadConfig(t *testing.T) {
4749
CheckInterval: time.Second * 5,
4850
CleanupOnStart: true,
4951
},
50-
Timeout: 2 * time.Second,
51-
FSync: true,
52+
Timeout: 2 * time.Second,
53+
FSync: true,
54+
CreateDirectory: false,
55+
DirectoryPermissions: "0750",
5256
},
5357
},
5458
}
@@ -95,6 +99,115 @@ func TestHandleProvidingFilePathAsDirWithAnError(t *testing.T) {
9599
require.Error(t, err)
96100
require.EqualError(t, err, file.Name()+" is not a directory")
97101
}
102+
func TestDirectoryCreateConfig(t *testing.T) {
103+
tests := []struct {
104+
name string
105+
config func(*testing.T, extension.Factory) *Config
106+
err error
107+
}{
108+
{
109+
name: "create directory true - no error",
110+
config: func(t *testing.T, f extension.Factory) *Config {
111+
storageDir := filepath.Join(t.TempDir(), uuid.NewString())
112+
cfg := f.CreateDefaultConfig().(*Config)
113+
cfg.Directory = storageDir
114+
cfg.CreateDirectory = true
115+
return cfg
116+
},
117+
err: nil,
118+
},
119+
{
120+
name: "create directory true - no error - 0700 permissions",
121+
config: func(t *testing.T, f extension.Factory) *Config {
122+
storageDir := filepath.Join(t.TempDir(), uuid.NewString())
123+
cfg := f.CreateDefaultConfig().(*Config)
124+
cfg.Directory = storageDir
125+
cfg.CreateDirectory = true
126+
cfg.DirectoryPermissions = "0700"
127+
return cfg
128+
},
129+
err: nil,
130+
},
131+
{
132+
name: "create directory false - error",
133+
config: func(t *testing.T, f extension.Factory) *Config {
134+
storageDir := filepath.Join(t.TempDir(), uuid.NewString())
135+
cfg := f.CreateDefaultConfig().(*Config)
136+
cfg.Directory = storageDir
137+
cfg.CreateDirectory = false
138+
return cfg
139+
},
140+
err: os.ErrNotExist,
141+
},
142+
{
143+
name: "create directory true - invalid permissions",
144+
config: func(t *testing.T, f extension.Factory) *Config {
145+
storageDir := filepath.Join(t.TempDir(), uuid.NewString())
146+
cfg := f.CreateDefaultConfig().(*Config)
147+
cfg.Directory = storageDir
148+
cfg.CreateDirectory = true
149+
cfg.DirectoryPermissions = "invalid string"
150+
return cfg
151+
},
152+
err: errInvalidOctal,
153+
},
154+
{
155+
name: "create directory true - rwxr--r-- (should be octal string)",
156+
config: func(t *testing.T, f extension.Factory) *Config {
157+
storageDir := filepath.Join(t.TempDir(), uuid.NewString())
158+
cfg := f.CreateDefaultConfig().(*Config)
159+
cfg.Directory = storageDir
160+
cfg.CreateDirectory = true
161+
cfg.DirectoryPermissions = "rwxr--r--"
162+
return cfg
163+
},
164+
err: errInvalidOctal,
165+
},
166+
{
167+
name: "create directory true - 0778 (invalid octal)",
168+
config: func(t *testing.T, f extension.Factory) *Config {
169+
storageDir := filepath.Join(t.TempDir(), uuid.NewString())
170+
cfg := f.CreateDefaultConfig().(*Config)
171+
cfg.Directory = storageDir
172+
cfg.CreateDirectory = true
173+
cfg.DirectoryPermissions = "0778"
174+
return cfg
175+
},
176+
err: errInvalidOctal,
177+
},
178+
{
179+
name: "create directory true - 07771 (invalid permission bits)",
180+
config: func(t *testing.T, f extension.Factory) *Config {
181+
storageDir := filepath.Join(t.TempDir(), uuid.NewString())
182+
cfg := f.CreateDefaultConfig().(*Config)
183+
cfg.Directory = storageDir
184+
cfg.CreateDirectory = true
185+
cfg.DirectoryPermissions = "07771"
186+
return cfg
187+
},
188+
err: errInvalidPermissionBits,
189+
},
190+
{
191+
name: "create directory false - 07771 (invalid string) - no error",
192+
config: func(t *testing.T, f extension.Factory) *Config {
193+
cfg := f.CreateDefaultConfig().(*Config)
194+
cfg.Directory = t.TempDir()
195+
cfg.CreateDirectory = false
196+
cfg.DirectoryPermissions = "07771"
197+
return cfg
198+
199+
},
200+
err: nil,
201+
},
202+
}
203+
for _, tt := range tests {
204+
t.Run(tt.name, func(t *testing.T) {
205+
f := NewFactory()
206+
config := tt.config(t, f)
207+
require.ErrorIs(t, config.Validate(), tt.err)
208+
})
209+
}
210+
}
98211

99212
func TestCompactionDirectory(t *testing.T) {
100213
f := NewFactory()
@@ -157,5 +270,4 @@ func TestCompactionDirectory(t *testing.T) {
157270
require.ErrorIs(t, component.ValidateConfig(test.config(t)), test.err)
158271
})
159272
}
160-
161273
}

extension/storage/filestorage/extension.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,19 @@ type localFileStorage struct {
2626
var _ storage.Extension = (*localFileStorage)(nil)
2727

2828
func newLocalFileStorage(logger *zap.Logger, config *Config) (extension.Extension, error) {
29+
if config.CreateDirectory {
30+
var dirs []string
31+
if config.Compaction.OnStart || config.Compaction.OnRebound {
32+
dirs = []string{config.Directory, config.Compaction.Directory}
33+
} else {
34+
dirs = []string{config.Directory}
35+
}
36+
for _, dir := range dirs {
37+
if err := ensureDirectoryExists(dir, os.FileMode(config.directoryPermissionsParsed)); err != nil {
38+
return nil, err
39+
}
40+
}
41+
}
2942
return &localFileStorage{
3043
cfg: config,
3144
logger: logger,
@@ -129,6 +142,14 @@ func isSafe(character rune) bool {
129142
return false
130143
}
131144

145+
func ensureDirectoryExists(path string, perm os.FileMode) error {
146+
if _, err := os.Stat(path); os.IsNotExist(err) {
147+
return os.MkdirAll(path, perm)
148+
}
149+
// we already handled other errors in config.Validate(), so it's okay to return nil
150+
return nil
151+
}
152+
132153
// cleanup left compaction temporary files from previous killed process
133154
func (lfs *localFileStorage) cleanup(compactionDirectory string) error {
134155
pattern := filepath.Join(compactionDirectory, fmt.Sprintf("%s*", TempDbPrefix))

extension/storage/filestorage/extension_test.go

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,16 @@ import (
88
"fmt"
99
"os"
1010
"path/filepath"
11+
"runtime"
1112
"sync"
1213
"testing"
1314

15+
"github.com/google/uuid"
1416
"github.com/stretchr/testify/assert"
1517
"github.com/stretchr/testify/require"
1618
"go.opentelemetry.io/collector/component"
1719
"go.opentelemetry.io/collector/component/componenttest"
20+
"go.opentelemetry.io/collector/extension"
1821
"go.opentelemetry.io/collector/extension/experimental/storage"
1922
"go.opentelemetry.io/collector/extension/extensiontest"
2023
"go.uber.org/zap"
@@ -525,3 +528,89 @@ func TestCompactionOnStart(t *testing.T) {
525528
require.NoError(t, client.Close(context.TODO()))
526529
})
527530
}
531+
532+
func TestDirectoryCreation(t *testing.T) {
533+
tests := []struct {
534+
name string
535+
config func(*testing.T, extension.Factory) *Config
536+
validate func(*testing.T, *Config)
537+
}{
538+
{
539+
name: "create directory true - no error",
540+
config: func(t *testing.T, f extension.Factory) *Config {
541+
tempDir := t.TempDir()
542+
storageDir := filepath.Join(tempDir, uuid.NewString())
543+
cfg := f.CreateDefaultConfig().(*Config)
544+
cfg.Directory = storageDir
545+
cfg.CreateDirectory = true
546+
cfg.DirectoryPermissions = "0750"
547+
require.NoError(t, cfg.Validate())
548+
return cfg
549+
},
550+
validate: func(t *testing.T, cfg *Config) {
551+
require.DirExists(t, cfg.Directory)
552+
s, err := os.Stat(cfg.Directory)
553+
require.NoError(t, err)
554+
var expectedFileMode os.FileMode
555+
if runtime.GOOS == "windows" { // on Windows, we get 0777 for writable directories
556+
expectedFileMode = os.FileMode(0777)
557+
} else {
558+
expectedFileMode = os.FileMode(0750)
559+
}
560+
require.Equal(t, expectedFileMode, s.Mode()&os.ModePerm)
561+
},
562+
},
563+
{
564+
name: "create directory true - no error - 0700 permissions",
565+
config: func(t *testing.T, f extension.Factory) *Config {
566+
tempDir := t.TempDir()
567+
storageDir := filepath.Join(tempDir, uuid.NewString())
568+
cfg := f.CreateDefaultConfig().(*Config)
569+
cfg.Directory = storageDir
570+
cfg.DirectoryPermissions = "0700"
571+
cfg.CreateDirectory = true
572+
require.NoError(t, cfg.Validate())
573+
return cfg
574+
},
575+
validate: func(t *testing.T, cfg *Config) {
576+
require.DirExists(t, cfg.Directory)
577+
s, err := os.Stat(cfg.Directory)
578+
require.NoError(t, err)
579+
var expectedFileMode os.FileMode
580+
if runtime.GOOS == "windows" { // on Windows, we get 0777 for writable directories
581+
expectedFileMode = os.FileMode(0777)
582+
} else {
583+
expectedFileMode = os.FileMode(0700)
584+
}
585+
require.Equal(t, expectedFileMode, s.Mode()&os.ModePerm)
586+
},
587+
},
588+
{
589+
name: "create directory false - error",
590+
config: func(t *testing.T, f extension.Factory) *Config {
591+
tempDir := t.TempDir()
592+
storageDir := filepath.Join(tempDir, uuid.NewString())
593+
cfg := f.CreateDefaultConfig().(*Config)
594+
cfg.Directory = storageDir
595+
cfg.CreateDirectory = false
596+
require.ErrorIs(t, cfg.Validate(), os.ErrNotExist)
597+
return cfg
598+
},
599+
validate: func(t *testing.T, cfg *Config) {
600+
require.NoDirExists(t, cfg.Directory)
601+
},
602+
},
603+
}
604+
for _, tt := range tests {
605+
t.Run(tt.name, func(t *testing.T) {
606+
f := NewFactory()
607+
config := tt.config(t, f)
608+
if config != nil {
609+
ext, err := f.CreateExtension(context.Background(), extensiontest.NewNopSettings(), config)
610+
require.NoError(t, err)
611+
require.NotNil(t, ext)
612+
tt.validate(t, config)
613+
}
614+
})
615+
}
616+
}

extension/storage/filestorage/factory.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,10 @@ func createDefaultConfig() component.Config {
4747
CheckInterval: defaultCompactionInterval,
4848
CleanupOnStart: false,
4949
},
50-
Timeout: time.Second,
51-
FSync: false,
50+
Timeout: time.Second,
51+
FSync: false,
52+
CreateDirectory: false,
53+
DirectoryPermissions: "0750",
5254
}
5355
}
5456

0 commit comments

Comments
 (0)