S3: S3 Object Retention API to include XML namespace support (#7517)
* Refactor S3 Object Retention API to include XML namespace support and improve compatibility with Veeam. Updated XML tags to remove hardcoded namespaces and added test cases for retention and legal hold configurations without namespaces. * Added XMLNS field setting in both places
This commit is contained in:
@@ -23,6 +23,11 @@ import (
|
|||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// S3 XML namespace
|
||||||
|
const (
|
||||||
|
S3Namespace = "http://s3.amazonaws.com/doc/2006-03-01/"
|
||||||
|
)
|
||||||
|
|
||||||
// Standard S3 HTTP request constants
|
// Standard S3 HTTP request constants
|
||||||
const (
|
const (
|
||||||
// S3 storage class
|
// S3 storage class
|
||||||
|
|||||||
@@ -86,6 +86,9 @@ func (s3a *S3ApiServer) GetObjectLockConfigurationHandler(w http.ResponseWriter,
|
|||||||
|
|
||||||
// Check if we have cached Object Lock configuration
|
// Check if we have cached Object Lock configuration
|
||||||
if bucketConfig.ObjectLockConfig != nil {
|
if bucketConfig.ObjectLockConfig != nil {
|
||||||
|
// Set namespace for S3 compatibility
|
||||||
|
bucketConfig.ObjectLockConfig.XMLNS = s3_constants.S3Namespace
|
||||||
|
|
||||||
// Use cached configuration and marshal it to XML for response
|
// Use cached configuration and marshal it to XML for response
|
||||||
marshaledXML, err := xml.Marshal(bucketConfig.ObjectLockConfig)
|
marshaledXML, err := xml.Marshal(bucketConfig.ObjectLockConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -139,6 +142,9 @@ func (s3a *S3ApiServer) GetObjectLockConfigurationHandler(w http.ResponseWriter,
|
|||||||
// not just ObjectLockConfig, before resetting the TTL
|
// not just ObjectLockConfig, before resetting the TTL
|
||||||
s3a.updateBucketConfigCacheFromEntry(freshEntry)
|
s3a.updateBucketConfigCacheFromEntry(freshEntry)
|
||||||
|
|
||||||
|
// Set namespace for S3 compatibility
|
||||||
|
objectLockConfig.XMLNS = s3_constants.S3Namespace
|
||||||
|
|
||||||
// Marshal and return the configuration
|
// Marshal and return the configuration
|
||||||
marshaledXML, err := xml.Marshal(objectLockConfig)
|
marshaledXML, err := xml.Marshal(objectLockConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -57,37 +57,40 @@ const (
|
|||||||
|
|
||||||
// ObjectRetention represents S3 Object Retention configuration
|
// ObjectRetention represents S3 Object Retention configuration
|
||||||
type ObjectRetention struct {
|
type ObjectRetention struct {
|
||||||
XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ Retention"`
|
XMLNS string `xml:"xmlns,attr,omitempty"`
|
||||||
Mode string `xml:"http://s3.amazonaws.com/doc/2006-03-01/ Mode,omitempty"`
|
XMLName xml.Name `xml:"Retention"`
|
||||||
RetainUntilDate *time.Time `xml:"http://s3.amazonaws.com/doc/2006-03-01/ RetainUntilDate,omitempty"`
|
Mode string `xml:"Mode,omitempty"`
|
||||||
|
RetainUntilDate *time.Time `xml:"RetainUntilDate,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ObjectLegalHold represents S3 Object Legal Hold configuration
|
// ObjectLegalHold represents S3 Object Legal Hold configuration
|
||||||
type ObjectLegalHold struct {
|
type ObjectLegalHold struct {
|
||||||
XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ LegalHold"`
|
XMLNS string `xml:"xmlns,attr,omitempty"`
|
||||||
Status string `xml:"http://s3.amazonaws.com/doc/2006-03-01/ Status,omitempty"`
|
XMLName xml.Name `xml:"LegalHold"`
|
||||||
|
Status string `xml:"Status,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ObjectLockConfiguration represents S3 Object Lock Configuration
|
// ObjectLockConfiguration represents S3 Object Lock Configuration
|
||||||
type ObjectLockConfiguration struct {
|
type ObjectLockConfiguration struct {
|
||||||
XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ ObjectLockConfiguration"`
|
XMLNS string `xml:"xmlns,attr,omitempty"`
|
||||||
ObjectLockEnabled string `xml:"http://s3.amazonaws.com/doc/2006-03-01/ ObjectLockEnabled,omitempty"`
|
XMLName xml.Name `xml:"ObjectLockConfiguration"`
|
||||||
Rule *ObjectLockRule `xml:"http://s3.amazonaws.com/doc/2006-03-01/ Rule,omitempty"`
|
ObjectLockEnabled string `xml:"ObjectLockEnabled,omitempty"`
|
||||||
|
Rule *ObjectLockRule `xml:"Rule,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ObjectLockRule represents an Object Lock Rule
|
// ObjectLockRule represents an Object Lock Rule
|
||||||
type ObjectLockRule struct {
|
type ObjectLockRule struct {
|
||||||
XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ Rule"`
|
XMLName xml.Name `xml:"Rule"`
|
||||||
DefaultRetention *DefaultRetention `xml:"http://s3.amazonaws.com/doc/2006-03-01/ DefaultRetention,omitempty"`
|
DefaultRetention *DefaultRetention `xml:"DefaultRetention,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultRetention represents default retention settings
|
// DefaultRetention represents default retention settings
|
||||||
// Implements custom XML unmarshal to track if Days/Years were present in XML
|
// Implements custom XML unmarshal to track if Days/Years were present in XML
|
||||||
type DefaultRetention struct {
|
type DefaultRetention struct {
|
||||||
XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ DefaultRetention"`
|
XMLName xml.Name `xml:"DefaultRetention"`
|
||||||
Mode string `xml:"http://s3.amazonaws.com/doc/2006-03-01/ Mode,omitempty"`
|
Mode string `xml:"Mode,omitempty"`
|
||||||
Days int `xml:"http://s3.amazonaws.com/doc/2006-03-01/ Days,omitempty"`
|
Days int `xml:"Days,omitempty"`
|
||||||
Years int `xml:"http://s3.amazonaws.com/doc/2006-03-01/ Years,omitempty"`
|
Years int `xml:"Years,omitempty"`
|
||||||
DaysSet bool `xml:"-"`
|
DaysSet bool `xml:"-"`
|
||||||
YearsSet bool `xml:"-"`
|
YearsSet bool `xml:"-"`
|
||||||
}
|
}
|
||||||
@@ -102,8 +105,8 @@ func (dr *DefaultRetention) UnmarshalXML(d *xml.Decoder, start xml.StartElement)
|
|||||||
type Alias DefaultRetention
|
type Alias DefaultRetention
|
||||||
aux := &struct {
|
aux := &struct {
|
||||||
*Alias
|
*Alias
|
||||||
Days *int `xml:"http://s3.amazonaws.com/doc/2006-03-01/ Days,omitempty"`
|
Days *int `xml:"Days,omitempty"`
|
||||||
Years *int `xml:"http://s3.amazonaws.com/doc/2006-03-01/ Years,omitempty"`
|
Years *int `xml:"Years,omitempty"`
|
||||||
}{Alias: (*Alias)(dr)}
|
}{Alias: (*Alias)(dr)}
|
||||||
if err := d.DecodeElement(aux, &start); err != nil {
|
if err := d.DecodeElement(aux, &start); err != nil {
|
||||||
glog.V(2).Infof("DefaultRetention.UnmarshalXML: decode error: %v", err)
|
glog.V(2).Infof("DefaultRetention.UnmarshalXML: decode error: %v", err)
|
||||||
@@ -245,6 +248,8 @@ func (s3a *S3ApiServer) getObjectRetention(bucket, object, versionId string) (*O
|
|||||||
return nil, ErrNoRetentionConfiguration
|
return nil, ErrNoRetentionConfiguration
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set namespace for S3 compatibility
|
||||||
|
retention.XMLNS = s3_constants.S3Namespace
|
||||||
return retention, nil
|
return retention, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -386,6 +391,8 @@ func (s3a *S3ApiServer) getObjectLegalHold(bucket, object, versionId string) (*O
|
|||||||
return nil, ErrNoLegalHoldConfiguration
|
return nil, ErrNoLegalHoldConfiguration
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set namespace for S3 compatibility
|
||||||
|
legalHold.XMLNS = s3_constants.S3Namespace
|
||||||
return legalHold, nil
|
return legalHold, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -201,6 +201,30 @@ func TestParseObjectRetention(t *testing.T) {
|
|||||||
RetainUntilDate: timePtr(time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)),
|
RetainUntilDate: timePtr(time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "Valid retention XML without namespace (Veeam compatibility)",
|
||||||
|
xmlBody: `<Retention>
|
||||||
|
<Mode>GOVERNANCE</Mode>
|
||||||
|
<RetainUntilDate>2024-12-31T23:59:59Z</RetainUntilDate>
|
||||||
|
</Retention>`,
|
||||||
|
expectError: false,
|
||||||
|
expectedResult: &ObjectRetention{
|
||||||
|
Mode: "GOVERNANCE",
|
||||||
|
RetainUntilDate: timePtr(time.Date(2024, 12, 31, 23, 59, 59, 0, time.UTC)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Valid compliance retention XML without namespace (Veeam compatibility)",
|
||||||
|
xmlBody: `<Retention>
|
||||||
|
<Mode>COMPLIANCE</Mode>
|
||||||
|
<RetainUntilDate>2025-01-01T00:00:00Z</RetainUntilDate>
|
||||||
|
</Retention>`,
|
||||||
|
expectError: false,
|
||||||
|
expectedResult: &ObjectRetention{
|
||||||
|
Mode: "COMPLIANCE",
|
||||||
|
RetainUntilDate: timePtr(time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)),
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "Empty XML body",
|
name: "Empty XML body",
|
||||||
xmlBody: "",
|
xmlBody: "",
|
||||||
@@ -311,6 +335,26 @@ func TestParseObjectLegalHold(t *testing.T) {
|
|||||||
Status: "OFF",
|
Status: "OFF",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "Valid legal hold ON without namespace",
|
||||||
|
xmlBody: `<LegalHold>
|
||||||
|
<Status>ON</Status>
|
||||||
|
</LegalHold>`,
|
||||||
|
expectError: false,
|
||||||
|
expectedResult: &ObjectLegalHold{
|
||||||
|
Status: "ON",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Valid legal hold OFF without namespace",
|
||||||
|
xmlBody: `<LegalHold>
|
||||||
|
<Status>OFF</Status>
|
||||||
|
</LegalHold>`,
|
||||||
|
expectError: false,
|
||||||
|
expectedResult: &ObjectLegalHold{
|
||||||
|
Status: "OFF",
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "Empty XML body",
|
name: "Empty XML body",
|
||||||
xmlBody: "",
|
xmlBody: "",
|
||||||
@@ -405,6 +449,38 @@ func TestParseObjectLockConfiguration(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "Valid object lock configuration without namespace",
|
||||||
|
xmlBody: `<ObjectLockConfiguration>
|
||||||
|
<ObjectLockEnabled>Enabled</ObjectLockEnabled>
|
||||||
|
</ObjectLockConfiguration>`,
|
||||||
|
expectError: false,
|
||||||
|
expectedResult: &ObjectLockConfiguration{
|
||||||
|
ObjectLockEnabled: "Enabled",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Valid object lock configuration with rule without namespace",
|
||||||
|
xmlBody: `<ObjectLockConfiguration>
|
||||||
|
<ObjectLockEnabled>Enabled</ObjectLockEnabled>
|
||||||
|
<Rule>
|
||||||
|
<DefaultRetention>
|
||||||
|
<Mode>GOVERNANCE</Mode>
|
||||||
|
<Days>30</Days>
|
||||||
|
</DefaultRetention>
|
||||||
|
</Rule>
|
||||||
|
</ObjectLockConfiguration>`,
|
||||||
|
expectError: false,
|
||||||
|
expectedResult: &ObjectLockConfiguration{
|
||||||
|
ObjectLockEnabled: "Enabled",
|
||||||
|
Rule: &ObjectLockRule{
|
||||||
|
DefaultRetention: &DefaultRetention{
|
||||||
|
Mode: "GOVERNANCE",
|
||||||
|
Days: 30,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "Empty XML body",
|
name: "Empty XML body",
|
||||||
xmlBody: "",
|
xmlBody: "",
|
||||||
|
|||||||
Reference in New Issue
Block a user