Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace os.stat sentinel file check with XRootD http GET #1791

Merged
merged 4 commits into from
Dec 5, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions launchers/launcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,13 @@ func LaunchModules(ctx context.Context, modules server_structs.ServerType) (serv

if modules.IsEnabled(server_structs.OriginType) {

var server server_structs.XRootDServer
server, err = OriginServe(ctx, engine, egrp, modules)
if err != nil {
return
}
servers = append(servers, server)

var originExports []server_utils.OriginExport
originExports, err = server_utils.GetOriginExports()
if err != nil {
Expand All @@ -167,13 +174,6 @@ func LaunchModules(ctx context.Context, modules server_structs.ServerType) (serv
return
}

var server server_structs.XRootDServer
server, err = OriginServe(ctx, engine, egrp, modules)
if err != nil {
return
}
servers = append(servers, server)

// Ordering: `LaunchBrokerListener` depends on the "right" value of Origin.FederationPrefix
// which is possibly not set until `OriginServe` is called.
// NOTE: Until the Broker supports multi-export origins, we've made the assumption that there
Expand Down
50 changes: 47 additions & 3 deletions server_utils/origin.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,22 @@ package server_utils

import (
"fmt"
"os"
"net/http"
"path"
"path/filepath"
"reflect"
"strings"
"time"

"github.com/mitchellh/mapstructure"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
"github.com/spf13/viper"

"github.com/pelicanplatform/pelican/config"
"github.com/pelicanplatform/pelican/param"
"github.com/pelicanplatform/pelican/server_structs"
"github.com/pelicanplatform/pelican/token"
)

var originExports []OriginExport
Expand Down Expand Up @@ -750,6 +753,29 @@ from S3 service URL. In this configuration, objects can be accessed at /federati
return originExports, nil
}

// Generate a test auth token for checking the sentinel location
jhiemstrawisc marked this conversation as resolved.
Show resolved Hide resolved
func generateFileTestScitoken(resourceScope string) (string, error) {
jhiemstrawisc marked this conversation as resolved.
Show resolved Hide resolved
issuerUrl := param.Server_ExternalWebUrl.GetString()
if issuerUrl == "" { // if both are empty, then error
return "", errors.New("failed to create token: invalid iss, Server_ExternalWebUrl is empty")
jhiemstrawisc marked this conversation as resolved.
Show resolved Hide resolved
}
fTestTokenCfg := token.NewWLCGToken()
fTestTokenCfg.Lifetime = time.Minute
fTestTokenCfg.Issuer = issuerUrl
fTestTokenCfg.Subject = "origin"
fTestTokenCfg.Claims = map[string]string{"scope": fmt.Sprintf("storage.read:/%v", resourceScope)}
// For self-tests, the audience is the server itself
fTestTokenCfg.AddAudienceAny()

// CreateToken also handles validation for us
tok, err := fTestTokenCfg.CreateToken()
if err != nil {
return "", errors.Wrap(err, "failed to create file test token")
jhiemstrawisc marked this conversation as resolved.
Show resolved Hide resolved
}

return tok, nil
}

// Check the sentinel files from Origin.Exports
func CheckOriginSentinelLocations(exports []OriginExport) (ok bool, err error) {
for _, export := range exports {
Expand All @@ -758,11 +784,29 @@ func CheckOriginSentinelLocations(exports []OriginExport) (ok bool, err error) {
if path.Base(sentinelPath) != sentinelPath {
return false, errors.Errorf("invalid SentinelLocation path for StoragePrefix %s, file must not contain a directory. Got %s", export.StoragePrefix, export.SentinelLocation)
}
fullPath := filepath.Join(export.StoragePrefix, sentinelPath)
_, err := os.Stat(fullPath)

jhiemstrawisc marked this conversation as resolved.
Show resolved Hide resolved
fullPath := filepath.Join(export.FederationPrefix, sentinelPath)
tkn, err := generateFileTestScitoken(sentinelPath)
jhiemstrawisc marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return false, errors.Wrap(err, "Failed to generate self-auth token for sentinel file check")
jhiemstrawisc marked this conversation as resolved.
Show resolved Hide resolved
}

sentinelUrl := fmt.Sprintf("%v%v", param.Origin_Url.GetString(), fullPath)
jhiemstrawisc marked this conversation as resolved.
Show resolved Hide resolved
req, err := http.NewRequest(http.MethodGet, sentinelUrl, nil)
if err != nil {
return false, errors.Wrap(err, "Failed to create GET request for sentinel file check")
jhiemstrawisc marked this conversation as resolved.
Show resolved Hide resolved
}
req.Header.Set("Authorization", "Bearer "+tkn)

client := http.Client{Transport: config.GetTransport()}
resp, err := client.Do(req)
if err != nil {
return false, errors.Wrapf(err, "fail to open SentinelLocation %s for StoragePrefix %s. Collection check failed", export.SentinelLocation, export.StoragePrefix)
jhiemstrawisc marked this conversation as resolved.
Show resolved Hide resolved
}

if resp.StatusCode != 200 {
return false, errors.New(fmt.Sprintf("Got non-200 response code %v when checking SentinelLocation %s for StoragePrefix %s", resp.StatusCode, export.SentinelLocation, export.StoragePrefix))
}
}
}
return true, nil
Expand Down
60 changes: 0 additions & 60 deletions server_utils/origin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,6 @@ package server_utils
import (
_ "embed"
"fmt"
"os"
"path/filepath"
"strings"
"testing"

Expand Down Expand Up @@ -395,64 +393,6 @@ func TestGetExports(t *testing.T) {
})
}

func TestCheckOriginSentinelLocation(t *testing.T) {
tmpDir := t.TempDir()
tempStn := filepath.Join(tmpDir, "mock_sentinel")
file, err := os.Create(tempStn)
require.NoError(t, err)
err = file.Close()
require.NoError(t, err)

mockExportNoStn := OriginExport{
StoragePrefix: "/foo/bar",
FederationPrefix: "/demo/foo/bar",
Capabilities: server_structs.Capabilities{Reads: true},
}
mockExportValidStn := OriginExport{
StoragePrefix: tmpDir,
FederationPrefix: "/demo/foo/bar",
Capabilities: server_structs.Capabilities{Reads: true},
SentinelLocation: "mock_sentinel",
}
mockExportInvalidStn := OriginExport{
StoragePrefix: tmpDir,
FederationPrefix: "/demo/foo/bar",
Capabilities: server_structs.Capabilities{Reads: true},
SentinelLocation: "sentinel_dne",
}

t.Run("empty-sentinel-return-ok", func(t *testing.T) {
exports := make([]OriginExport, 0)
exports = append(exports, mockExportNoStn)
exports = append(exports, mockExportNoStn)

ok, err := CheckOriginSentinelLocations(exports)
assert.NoError(t, err)
assert.True(t, ok)
})

t.Run("valid-sentinel-return-ok", func(t *testing.T) {
exports := make([]OriginExport, 0)
exports = append(exports, mockExportNoStn)
exports = append(exports, mockExportValidStn)

ok, err := CheckOriginSentinelLocations(exports)
assert.NoError(t, err)
assert.True(t, ok)
})

t.Run("invalid-sentinel-return-error", func(t *testing.T) {
exports := make([]OriginExport, 0)
exports = append(exports, mockExportNoStn)
exports = append(exports, mockExportValidStn)
exports = append(exports, mockExportInvalidStn)

ok, err := CheckOriginSentinelLocations(exports)
assert.Error(t, err)
assert.False(t, ok)
})
}

func runBucketNameTest(t *testing.T, name string, valid bool) {
t.Run(fmt.Sprintf("testBucketNameValidation-%s", name), func(t *testing.T) {
err := validateBucketName(name)
Expand Down
171 changes: 160 additions & 11 deletions xrootd/origin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -231,16 +231,7 @@ func TestMultiExportOrigin(t *testing.T) {
require.True(t, ok)
}

func runS3Test(t *testing.T, bucketName, urlStyle, objectName string) {
ctx, cancel, egrp := test_utils.TestContext(context.Background(), t)
defer func() { require.NoError(t, egrp.Wait()) }()
defer cancel()

server_utils.ResetTestState()

defer server_utils.ResetTestState()

federationPrefix := "/test"
func mockupS3Origin(ctx context.Context, egrp *errgroup.Group, t *testing.T, federationPrefix, bucketName, urlStyle string) context.CancelFunc {
regionName := "us-east-1"
serviceUrl := "https://s3.amazonaws.com"
viper.Set("Origin.FederationPrefix", federationPrefix)
Expand All @@ -260,7 +251,19 @@ func runS3Test(t *testing.T, bucketName, urlStyle, objectName string) {
viper.Set("Server.WebPort", 0)
viper.Set("TLSSkipVerify", true)

mockupCancel := originMockup(ctx, egrp, t)
return originMockup(ctx, egrp, t)
}

func runS3Test(t *testing.T, bucketName, urlStyle, objectName string) {
ctx, cancel, egrp := test_utils.TestContext(context.Background(), t)
defer func() { require.NoError(t, egrp.Wait()) }()
defer cancel()
server_utils.ResetTestState()
defer server_utils.ResetTestState()

federationPrefix := "/test"

mockupCancel := mockupS3Origin(ctx, egrp, t, federationPrefix, bucketName, urlStyle)
defer mockupCancel()

originEndpoint := param.Origin_Url.GetString()
Expand Down Expand Up @@ -304,3 +307,149 @@ func TestS3OriginConfig(t *testing.T) {
runS3Test(t, "", "path", "noaa-wod-pds/MD5SUMS")
})
}

func TestS3OriginWithSentinel(t *testing.T) {
ctx, cancel, egrp := test_utils.TestContext(context.Background(), t)
defer func() { require.NoError(t, egrp.Wait()) }()
defer cancel()
server_utils.ResetTestState()
defer server_utils.ResetTestState()

federationPrefix := "/test"
bucketName := "noaa-wod-pds"

mockupCancel := mockupS3Origin(ctx, egrp, t, federationPrefix, bucketName, "path")
defer mockupCancel()

mockExportValidStn := server_utils.OriginExport{
StoragePrefix: viper.GetString("Origin.StoragePrefix"),
FederationPrefix: viper.GetString("Origin.FederationPrefix"),
Capabilities: server_structs.Capabilities{Reads: true},
SentinelLocation: "MD5SUMS",
}

originEndpoint := param.Origin_Url.GetString()
// At this point, a 403 means the server is running, which means its ready to grab objects from
err := server_utils.WaitUntilWorking(ctx, "GET", originEndpoint, "xrootd", 403, true)
if err != nil {
t.Fatalf("Unsuccessful test: Server encountered an error: %v", err)
}

// mock export with no sentinel
mockExportNoStn := server_utils.OriginExport{
StoragePrefix: viper.GetString("Origin.StoragePrefix"),
FederationPrefix: viper.GetString("Origin.FederationPrefix"),
Capabilities: server_structs.Capabilities{Reads: true},
}

// mock export with an invalid sentinel
mockExportInvalidStn := server_utils.OriginExport{
StoragePrefix: viper.GetString("Origin.StoragePrefix"),
FederationPrefix: viper.GetString("Origin.FederationPrefix"),
Capabilities: server_structs.Capabilities{Reads: true},
SentinelLocation: "MD5SUMS_dne",
}

t.Run("valid-sentinel-return-ok", func(t *testing.T) {
ok, err := server_utils.CheckOriginSentinelLocations([]server_utils.OriginExport{mockExportValidStn})
require.NoError(t, err)
require.True(t, ok)
})
t.Run("empty-sentinel-return-ok", func(t *testing.T) {
ok, err := server_utils.CheckOriginSentinelLocations([]server_utils.OriginExport{mockExportNoStn})
require.NoError(t, err)
require.True(t, ok)
})

t.Run("invalid-sentinel-return-error", func(t *testing.T) {
ok, err := server_utils.CheckOriginSentinelLocations([]server_utils.OriginExport{mockExportInvalidStn})
require.Error(t, err)
require.False(t, ok)
})
}

func TestPosixOriginWithSentinel(t *testing.T) {
ctx, cancel, egrp := test_utils.TestContext(context.Background(), t)
defer func() { require.NoError(t, egrp.Wait()) }()
defer cancel()

server_utils.ResetTestState()

defer server_utils.ResetTestState()

// Create a test temp dir, ensure it's readable by XRootD
tmpPathPattern := "XRD-Tst_Orgn*"
tmpPath, err := os.MkdirTemp("", tmpPathPattern)
require.NoError(t, err)
err = os.Chmod(tmpPath, 0755)
require.NoError(t, err)

viper.Set("Origin.StoragePrefix", tmpPath)
viper.Set("Origin.FederationPrefix", "/test")
viper.Set("Origin.StorageType", "posix")
// Disable functionality we're not using (and is difficult to make work on Mac)
viper.Set("Origin.EnableCmsd", false)
viper.Set("Origin.EnableMacaroons", false)
viper.Set("Origin.EnableVoms", false)
viper.Set("Origin.Port", 0)
viper.Set("Server.WebPort", 0)
viper.Set("TLSSkipVerify", true)
viper.Set("Logging.Origin.Scitokens", "trace")

mockupCancel := originMockup(ctx, egrp, t)
defer mockupCancel()

// mock export with a valid sentinel
mockExportValidStn := server_utils.OriginExport{
StoragePrefix: viper.GetString("Origin.StoragePrefix"),
FederationPrefix: viper.GetString("Origin.FederationPrefix"),
Capabilities: server_structs.Capabilities{Reads: true},
SentinelLocation: "mock_sentinel",
}
// mock export with no sentinel
mockExportNoStn := server_utils.OriginExport{
StoragePrefix: viper.GetString("Origin.StoragePrefix"),
FederationPrefix: viper.GetString("Origin.FederationPrefix"),
Capabilities: server_structs.Capabilities{Reads: true},
}
// mock export with an invalid sentinel
mockExportInvalidStn := server_utils.OriginExport{
StoragePrefix: viper.GetString("Origin.StoragePrefix"),
FederationPrefix: viper.GetString("Origin.FederationPrefix"),
Capabilities: server_structs.Capabilities{Reads: true},
SentinelLocation: "sentinel_dne",
}

// Create a sentinel file, ensure it's readable by XRootD
tempStn := filepath.Join(mockExportValidStn.StoragePrefix, mockExportValidStn.SentinelLocation)
file, err := os.Create(tempStn)
require.NoError(t, err)
err = file.Close()
require.NoError(t, err)
err = os.Chmod(tempStn, 0755)
require.NoError(t, err)

err = server_utils.WaitUntilWorking(ctx, "GET", param.Origin_Url.GetString(), "xrootd", 403, false)
if err != nil {
t.Fatalf("Unsuccessful test: Server encountered an error: %v", err)
}
require.NoError(t, err)

t.Run("valid-sentinel-return-ok", func(t *testing.T) {
ok, err := server_utils.CheckOriginSentinelLocations([]server_utils.OriginExport{mockExportValidStn})
require.NoError(t, err)
require.True(t, ok)
})

t.Run("empty-sentinel-return-ok", func(t *testing.T) {
ok, err := server_utils.CheckOriginSentinelLocations([]server_utils.OriginExport{mockExportNoStn})
require.NoError(t, err)
require.True(t, ok)
})

t.Run("invalid-sentinel-return-error", func(t *testing.T) {
ok, err := server_utils.CheckOriginSentinelLocations([]server_utils.OriginExport{mockExportInvalidStn})
require.Error(t, err)
require.False(t, ok)
})
}
Loading