Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
19 changes: 19 additions & 0 deletions client/fed_long_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,7 @@ func TestRecursiveUploadsAndDownloadsWithQuery(t *testing.T) {

// Test we work with just the query
t.Run("testRecursiveGetAndPutWithQuery", func(t *testing.T) {
t.Cleanup(test_utils.SetupTestLogging(t))
_, err := config.SetPreferredPrefix(config.PelicanPrefix)
require.NoError(t, err)

Expand Down Expand Up @@ -388,6 +389,7 @@ func TestRecursiveUploadsAndDownloadsWithQuery(t *testing.T) {

// Test we work with a value assigned to it (we print deprecation warning)
t.Run("testRecursiveGetAndPutWithQueryWithValueTrue", func(t *testing.T) {
t.Cleanup(test_utils.SetupTestLogging(t))
_, err := config.SetPreferredPrefix(config.PelicanPrefix)
require.NoError(t, err)

Expand Down Expand Up @@ -417,6 +419,7 @@ func TestRecursiveUploadsAndDownloadsWithQuery(t *testing.T) {

// Test we work with a value assigned to it but says recursive=false (we print deprecation warning and ignore arguments in query so we still work)
t.Run("testRecursiveGetAndPutWithQueryWithValueFalse", func(t *testing.T) {
t.Cleanup(test_utils.SetupTestLogging(t))
_, err := config.SetPreferredPrefix(config.PelicanPrefix)
require.NoError(t, err)

Expand Down Expand Up @@ -446,6 +449,7 @@ func TestRecursiveUploadsAndDownloadsWithQuery(t *testing.T) {

// Test we work with both recursive and directread query params
t.Run("testRecursiveGetAndPutWithQueryAndDirectread", func(t *testing.T) {
t.Cleanup(test_utils.SetupTestLogging(t))
_, err := config.SetPreferredPrefix(config.PelicanPrefix)
require.NoError(t, err)

Expand Down Expand Up @@ -564,6 +568,7 @@ func TestSyncUpload(t *testing.T) {
innerTempFile.Close()

t.Run("testSyncUploadFull", func(t *testing.T) {
t.Cleanup(test_utils.SetupTestLogging(t))
// Set path for object to upload/download
tempPath := tempDir
dirName := filepath.Base(tempPath)
Expand All @@ -582,6 +587,7 @@ func TestSyncUpload(t *testing.T) {
})

t.Run("testSyncUploadNone", func(t *testing.T) {
t.Cleanup(test_utils.SetupTestLogging(t))
// Set path for object to upload/download
dirName := filepath.Base(tempDir)
uploadUrl := fmt.Sprintf("pelican://%s/first/namespace/sync_upload_none/%s", discoveryUrl.Host, dirName)
Expand All @@ -601,6 +607,7 @@ func TestSyncUpload(t *testing.T) {
})

t.Run("testSyncUploadPartial", func(t *testing.T) {
t.Cleanup(test_utils.SetupTestLogging(t))
// Set path for object to upload/download
dirName := filepath.Base(tempDir)
uploadUrl := fmt.Sprintf("pelican://%s/first/namespace/sync_upload_partial/%s", discoveryUrl.Host, dirName)
Expand Down Expand Up @@ -635,6 +642,7 @@ func TestSyncUpload(t *testing.T) {
})

t.Run("testSyncUploadFile", func(t *testing.T) {
t.Cleanup(test_utils.SetupTestLogging(t))
// Create a new test file to upload
newDir := t.TempDir()
newTestFile, err := os.CreateTemp(newDir, "newTest")
Expand Down Expand Up @@ -772,13 +780,15 @@ func TestSyncDownload(t *testing.T) {
verifySuccessfulTransfer(t, transferDetailsUpload)

t.Run("testSyncDownloadFull", func(t *testing.T) {
t.Cleanup(test_utils.SetupTestLogging(t))
// Download the files we just uploaded
transferDetailsDownload, err := client.DoGet(fed.Ctx, uploadUrl, t.TempDir(), true, client.WithTokenLocation(tempToken.Name()), client.WithSynchronize(client.SyncSize))
require.NoError(t, err)
verifySuccessfulTransfer(t, transferDetailsDownload)
})

t.Run("testSyncDownloadNone", func(t *testing.T) {
t.Cleanup(test_utils.SetupTestLogging(t))
// Set path for object to upload/download
dirName := t.TempDir()

Expand All @@ -794,6 +804,7 @@ func TestSyncDownload(t *testing.T) {
})

t.Run("testSyncDownloadObject", func(t *testing.T) {
t.Cleanup(test_utils.SetupTestLogging(t))
// Set path for object to upload/download
dirName := t.TempDir()
filename1 := filepath.Base(tempFile1.Name())
Expand Down Expand Up @@ -840,6 +851,7 @@ func TestSyncDownload(t *testing.T) {
})

t.Run("testSyncDownloadPartial", func(t *testing.T) {
t.Cleanup(test_utils.SetupTestLogging(t))
// Set path for object to upload/download
downloadDir := t.TempDir()
dirName := filepath.Base(tempDir)
Expand Down Expand Up @@ -954,6 +966,7 @@ func TestObjectPutNonRecursiveDirPath(t *testing.T) {
require.NoError(t, err)

t.Run("testObjectPutNonRecursiveDirPath", func(t *testing.T) {
t.Cleanup(test_utils.SetupTestLogging(t))

oldPref, err := config.SetPreferredPrefix(config.PelicanPrefix)
require.NoError(t, err)
Expand Down Expand Up @@ -1107,6 +1120,7 @@ func TestObjectDelete(t *testing.T) {
// Test deleting a non-existent object.
// The operation should fail with a 404 error indicating that the object does not exist.
t.Run("testDeleteNonExistentObject", func(t *testing.T) {
t.Cleanup(test_utils.SetupTestLogging(t))
doesNotExistPath := fmt.Sprintf("pelican://%s%s/doesNotExist", discoveryUrl.Host, "/with-write")
err := client.DoDelete(fed.Ctx, doesNotExistPath, false, client.WithTokenLocation(tempToken.Name()))
require.Error(t, err)
Expand All @@ -1121,6 +1135,7 @@ func TestObjectDelete(t *testing.T) {
// Test deleting an existing object.
// The operation should succeed, and the object should no longer be accessible.
t.Run("testDeleteObject", func(t *testing.T) {
t.Cleanup(test_utils.SetupTestLogging(t))
objectToDeletePelicanUrl := fmt.Sprintf("pelican://%s/with-write/%s", discoveryUrl.Host, filepath.Base(objectToBeDeleted))
err = client.DoDelete(fed.Ctx, objectToDeletePelicanUrl, false, client.WithTokenLocation(tempToken.Name()))
require.NoError(t, err)
Expand All @@ -1134,6 +1149,7 @@ func TestObjectDelete(t *testing.T) {
// Test deleting an empty collection.
// The operation should succeed, and the collection should no longer be accessible.
t.Run("testDeleteEmptyCollection", func(t *testing.T) {
t.Cleanup(test_utils.SetupTestLogging(t))
emptyCollectionPelicanUrl := fmt.Sprintf("pelican://%s/with-write/%s", discoveryUrl.Host, filepath.Base(emptyCollectionToBeDeleted))
err := client.DoDelete(fed.Ctx, emptyCollectionPelicanUrl, false, client.WithTokenLocation(tempToken.Name()))
require.NoError(t, err)
Expand All @@ -1146,6 +1162,7 @@ func TestObjectDelete(t *testing.T) {
// Test deleting a non-empty collection without the recursive flag.
// The operation should fail with an error indicating that the collection cannot be deleted because it is not empty, and the collection should still exist.
t.Run("testDeleteNonEmptyCollectionWithoutRecursive", func(t *testing.T) {
t.Cleanup(test_utils.SetupTestLogging(t))
collectionToDeletePelicanUrl := fmt.Sprintf("pelican://%s/with-write/%s", discoveryUrl.Host, filepath.Base(complexCollection))
err := client.DoDelete(fed.Ctx, collectionToDeletePelicanUrl, false, client.WithTokenLocation(tempToken.Name()))
require.Error(t, err)
Expand All @@ -1159,6 +1176,7 @@ func TestObjectDelete(t *testing.T) {
// Test deleting a non-empty collection with the recursive flag.
// The operation should succeed, and the collection and its contents should no longer be accessible.
t.Run("testDeleteNonEmptyCollectionWithRecursive", func(t *testing.T) {
t.Cleanup(test_utils.SetupTestLogging(t))
collectionToDeletePelicanUrl := fmt.Sprintf("pelican://%s/with-write/%s", discoveryUrl.Host, filepath.Base(complexCollection))
err = client.DoDelete(fed.Ctx, collectionToDeletePelicanUrl, true, client.WithTokenLocation(tempToken.Name()))
require.NoError(t, err)
Expand All @@ -1171,6 +1189,7 @@ func TestObjectDelete(t *testing.T) {
// Test attempting to delete an object in a prefix without writes capability.
// The operation should fail with a 403 permission error.
t.Run("testDeleteForNonWritableNamespace", func(t *testing.T) {
t.Cleanup(test_utils.SetupTestLogging(t))
collectionToDeletePelicanUrl := fmt.Sprintf("pelican://%s/without-write/%s", discoveryUrl.Host, filepath.Base(objectNonWritableNs))
err := client.DoDelete(fed.Ctx, collectionToDeletePelicanUrl, false, client.WithTokenLocation(tempToken.Name()))

Expand Down
108 changes: 108 additions & 0 deletions config/address_file.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/***************************************************************
*
* Copyright (C) 2025, 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 config

import (
"fmt"
"os"
"path/filepath"

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

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

// getServerRuntimeDir returns the runtime directory for server-wide runtime files using
// the configuration populated during server initialization.
func getServerRuntimeDir() (string, error) {
runtimeDir := viper.GetString(param.RuntimeDir.GetName())
if runtimeDir == "" {
return "", errors.New("runtime directory is not configured")
}
return runtimeDir, nil
}

// WriteAddressFile writes a file containing the actual addresses selected by Pelican
// after startup. This allows scripts to source the file to get the server's addresses
// instead of probing for ports (which can lead to race conditions).
//
// The file is written to the runtime directory and contains KEY=VALUE pairs,
// one per line, with the following keys:
// - SERVER_EXTERNAL_WEB_URL: The main web UI/API endpoint
// - ORIGIN_URL: The origin's XRootD endpoint (if origin is enabled)
// - CACHE_URL: The cache's XRootD endpoint (if cache is enabled)
//
// The file is written atomically using a temporary file and rename.
func WriteAddressFile(modules server_structs.ServerType) error {
runtimeDir, err := getServerRuntimeDir()
if err != nil {
return errors.Wrap(err, "failed to determine runtime directory")
}

// Ensure the runtime directory exists
if err := os.MkdirAll(runtimeDir, 0755); err != nil {
return errors.Wrap(err, "failed to create runtime directory")
}

addressFilePath := filepath.Join(runtimeDir, "pelican.addresses")
tempFilePath := addressFilePath + ".tmp"

// Build the content
var content string

// Always include the server external web URL
serverWebUrl := param.Server_ExternalWebUrl.GetString()
if serverWebUrl != "" {
content += fmt.Sprintf("SERVER_EXTERNAL_WEB_URL=%s\n", serverWebUrl)
}

// Include origin URL if origin is enabled
if modules.IsEnabled(server_structs.OriginType) {
originUrl := param.Origin_Url.GetString()
if originUrl != "" {
content += fmt.Sprintf("ORIGIN_URL=%s\n", originUrl)
}
}

// Include cache URL if cache is enabled
if modules.IsEnabled(server_structs.CacheType) {
cacheUrl := param.Cache_Url.GetString()
if cacheUrl != "" {
content += fmt.Sprintf("CACHE_URL=%s\n", cacheUrl)
}
}

// Write to temporary file first
if err := os.WriteFile(tempFilePath, []byte(content), 0600); err != nil {
return errors.Wrap(err, "failed to write temporary address file")
}

// Atomically rename the temporary file to the final location
if err := os.Rename(tempFilePath, addressFilePath); err != nil {
// Clean up the temp file if rename fails
os.Remove(tempFilePath)
return errors.Wrap(err, "failed to rename address file")
}

log.Infof("Address file written to %s", addressFilePath)
return nil
}
Loading
Loading