diff --git a/cache/cache_api.go b/cache/cache_api.go new file mode 100644 index 000000000..558509bee --- /dev/null +++ b/cache/cache_api.go @@ -0,0 +1,46 @@ +/*************************************************************** + * + * Copyright (C) 2024, Pelican Project, Morgridge Institute for Research + * + * 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 cache + +import ( + "os" + "path" + "path/filepath" + + "github.com/pelicanplatform/pelican/param" + "github.com/pkg/errors" +) + +// Check for the sentinel file +func CheckCacheSentinelLocation() error { + if param.Cache_SentinelLocation.IsSet() { + sentinelPath := param.Cache_SentinelLocation.GetString() + dataLoc := param.Cache_DataLocation.GetString() + sentinelPath = path.Clean(sentinelPath) + if path.Base(sentinelPath) != sentinelPath { + return errors.Errorf("invalid Cache.SentinelLocation path. File must not contain a directory. Got %s", sentinelPath) + } + fullPath := filepath.Join(dataLoc, sentinelPath) + _, err := os.Stat(fullPath) + if err != nil { + return errors.Wrapf(err, "failed to open Cache.SentinelLocation %s. Directory check failed", fullPath) + } + } + return nil +} diff --git a/cache/cache_api_test.go b/cache/cache_api_test.go new file mode 100644 index 000000000..d033dcc4d --- /dev/null +++ b/cache/cache_api_test.go @@ -0,0 +1,71 @@ +/*************************************************************** + * + * Copyright (C) 2024, Pelican Project, Morgridge Institute for Research + * + * 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 cache + +import ( + "os" + "path/filepath" + "testing" + + "github.com/pelicanplatform/pelican/param" + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCheckCacheSentinelLocation(t *testing.T) { + t.Run("sentinel-not-set", func(t *testing.T) { + viper.Reset() + err := CheckCacheSentinelLocation() + assert.NoError(t, err) + }) + + t.Run("sentinel-contains-dir", func(t *testing.T) { + viper.Reset() + viper.Set(param.Cache_SentinelLocation.GetName(), "/test.txt") + err := CheckCacheSentinelLocation() + require.Error(t, err) + assert.Equal(t, "invalid Cache.SentinelLocation path. File must not contain a directory. Got /test.txt", err.Error()) + }) + + t.Run("sentinel-dne", func(t *testing.T) { + tmpDir := t.TempDir() + viper.Reset() + viper.Set(param.Cache_SentinelLocation.GetName(), "test.txt") + viper.Set(param.Cache_DataLocation.GetName(), tmpDir) + err := CheckCacheSentinelLocation() + require.Error(t, err) + assert.Contains(t, err.Error(), "failed to open Cache.SentinelLocation") + }) + + t.Run("sentinel-exists", func(t *testing.T) { + tmpDir := t.TempDir() + viper.Reset() + + viper.Set(param.Cache_SentinelLocation.GetName(), "test.txt") + viper.Set(param.Cache_DataLocation.GetName(), tmpDir) + + file, err := os.Create(filepath.Join(tmpDir, "test.txt")) + require.NoError(t, err) + file.Close() + + err = CheckCacheSentinelLocation() + require.NoError(t, err) + }) +} diff --git a/docs/parameters.yaml b/docs/parameters.yaml index 98d2607e3..105584b43 100644 --- a/docs/parameters.yaml +++ b/docs/parameters.yaml @@ -459,7 +459,9 @@ description: |+ where each of these has the same effect as the corresponding "Origin.Enable*" configuration, except scoped to the given export. If "PublicReads" is included, "Reads" is inferred. - SentinelLocation: A filename under `StoragePrefix` path for Pelican to check the storage directory exists and is correctly mounted. - Leave it empty to skip the check. You should always choose a distinguished name for `SentinelLocation`. It should not be reused for other servers. + The value must be a file and contain no directory. Leave it empty to skip the check. + + You should always choose a distinct name for `SentinelLocation`. It should not be reused for other servers. If running in a containerized environment it should not be the name of the underlying physical host as that may change and lead to confusion. You need to manually create a file under path to `StoragePrefix` with the same name as `SentinelLocation`. Note that this parameter will be ignored if the origin StorageType is S3. @@ -939,6 +941,18 @@ root_default: /run/pelican/xrootd/cache default: $XDG_RUNTIME_DIR/pelican/cache components: ["cache"] --- +name: Cache.SentinelLocation +description: |+ + A filename under `Cache.DataLocation` path for Pelican to check the storage directory exists and is correctly mounted. + The value must be a file and contain no directory. Leave it empty to skip the check. + + You should always choose a distinct name for `Cache.SentinelLocation`. It should not be reused for other servers. + If running in a containerized environment it should not be the name of the underlying physical host as that may change and lead to confusion. + You need to manually create a file under path to `Cache.DataLocation` with the same name as `Cache.SentinelLocation`. +type: filename +default: none +components: ["cache"] +--- name: Cache.XRootDPrefix description: |+ The directory prefix for the XRootD cache configuration files. diff --git a/launchers/cache_serve.go b/launchers/cache_serve.go index 7d13b7b81..a91b72c8e 100644 --- a/launchers/cache_serve.go +++ b/launchers/cache_serve.go @@ -51,6 +51,10 @@ func CacheServe(ctx context.Context, engine *gin.Engine, egrp *errgroup.Group, m return nil, err } + if err := cache.CheckCacheSentinelLocation(); err != nil { + return nil, err + } + cache.RegisterCacheAPI(engine, ctx, egrp) cacheServer := &cache.CacheServer{} diff --git a/launchers/launcher.go b/launchers/launcher.go index 75c4c8911..cca3560c2 100644 --- a/launchers/launcher.go +++ b/launchers/launcher.go @@ -162,7 +162,7 @@ func LaunchModules(ctx context.Context, modules config.ServerType) (servers []se return } - ok, err = server_utils.CheckSentinelLocation(originExports) + ok, err = server_utils.CheckOriginSentinelLocations(originExports) if err != nil && !ok { return } diff --git a/param/parameters.go b/param/parameters.go index 1d205f580..744dce9a1 100644 --- a/param/parameters.go +++ b/param/parameters.go @@ -122,6 +122,7 @@ var ( Cache_HighWaterMark = StringParam{"Cache.HighWaterMark"} Cache_LowWatermark = StringParam{"Cache.LowWatermark"} Cache_RunLocation = StringParam{"Cache.RunLocation"} + Cache_SentinelLocation = StringParam{"Cache.SentinelLocation"} Cache_Url = StringParam{"Cache.Url"} Cache_XRootDPrefix = StringParam{"Cache.XRootDPrefix"} Director_DefaultResponse = StringParam{"Director.DefaultResponse"} diff --git a/param/parameters_struct.go b/param/parameters_struct.go index e1f4f014e..96ff64723 100644 --- a/param/parameters_struct.go +++ b/param/parameters_struct.go @@ -38,6 +38,7 @@ type Config struct { RunLocation string SelfTest bool SelfTestInterval time.Duration + SentinelLocation string Url string XRootDPrefix string } @@ -305,6 +306,7 @@ type configWithType struct { RunLocation struct { Type string; Value string } SelfTest struct { Type string; Value bool } SelfTestInterval struct { Type string; Value time.Duration } + SentinelLocation struct { Type string; Value string } Url struct { Type string; Value string } XRootDPrefix struct { Type string; Value string } } diff --git a/server_utils/origin.go b/server_utils/origin.go index e7b5af804..0687d71ee 100644 --- a/server_utils/origin.go +++ b/server_utils/origin.go @@ -492,7 +492,8 @@ from S3 service URL. In this configuration, objects can be accessed at /federati return originExports, nil } -func CheckSentinelLocation(exports []OriginExport) (ok bool, err error) { +// Check the sentinel files from Origin.Exports +func CheckOriginSentinelLocations(exports []OriginExport) (ok bool, err error) { for _, export := range exports { if export.SentinelLocation != "" { sentinelPath := path.Clean(export.SentinelLocation) diff --git a/server_utils/origin_test.go b/server_utils/origin_test.go index 355ebc1dc..346d192a2 100644 --- a/server_utils/origin_test.go +++ b/server_utils/origin_test.go @@ -406,7 +406,7 @@ func TestGetExports(t *testing.T) { }) } -func TestCheckSentinelLocation(t *testing.T) { +func TestCheckOriginSentinelLocation(t *testing.T) { tmpDir := t.TempDir() tempStn := filepath.Join(tmpDir, "mock_sentinel") file, err := os.Create(tempStn) @@ -437,7 +437,7 @@ func TestCheckSentinelLocation(t *testing.T) { exports = append(exports, mockExportNoStn) exports = append(exports, mockExportNoStn) - ok, err := CheckSentinelLocation(exports) + ok, err := CheckOriginSentinelLocations(exports) assert.NoError(t, err) assert.True(t, ok) }) @@ -447,7 +447,7 @@ func TestCheckSentinelLocation(t *testing.T) { exports = append(exports, mockExportNoStn) exports = append(exports, mockExportValidStn) - ok, err := CheckSentinelLocation(exports) + ok, err := CheckOriginSentinelLocations(exports) assert.NoError(t, err) assert.True(t, ok) }) @@ -458,7 +458,7 @@ func TestCheckSentinelLocation(t *testing.T) { exports = append(exports, mockExportValidStn) exports = append(exports, mockExportInvalidStn) - ok, err := CheckSentinelLocation(exports) + ok, err := CheckOriginSentinelLocations(exports) assert.Error(t, err) assert.False(t, ok) })