mirror of
https://github.com/mulgadc/spinifex.git
synced 2026-10-16 01:22:00 +00:00
feat: derive Encrypted from VBState.EncryptionEnabled, drop IsEncrypted reads
VolumeMetadata.IsEncrypted is being removed from viperblock — it was hardcoded false everywhere in spinifex and misrepresented the per-volume encryption status to AWS SDK callers. The authoritative flag is now VBState.EncryptionEnabled, persisted by viperblock at New() when MasterKey != nil. - volume CreateVolume drops the dead IsEncrypted struct member and surfaces Encrypted=(mkey != nil) on the EC2 response. - volume getVolumeByID now decodes the full VBState (falling back to a wrapper-only blob for legacy pre-encryption volumes) so it can read state.EncryptionEnabled directly. - snapshot CreateSnapshot pulls Encrypted from the source volume's VBState.EncryptionEnabled (already loaded one level up). - image synthesizeRootBlockDeviceMapping takes the cluster encryption posture (mkey != nil) since AMI metadata carries no per-image flag; cached masterkey load keeps the hot path cheap. - Test fixtures drop the now-defunct IsEncrypted lines; the volume fixture that asserts Encrypted=true switches to a full VBState seed with EncryptionEnabled=true.
This commit is contained in:
committed by
Bryn Mailer
parent
b5220996e1
commit
557d3e4d59
@@ -119,6 +119,7 @@ func (s *ImageServiceImpl) DescribeImages(input *ec2.DescribeImagesInput, accoun
|
||||
}
|
||||
|
||||
var images []*ec2.Image
|
||||
encryptedAtRest := s.clusterEncryptionEnabled()
|
||||
|
||||
// Iterate over CommonPrefixes to find ami-* directories
|
||||
for _, prefix := range result.CommonPrefixes {
|
||||
@@ -267,7 +268,7 @@ func (s *ImageServiceImpl) DescribeImages(input *ec2.DescribeImagesInput, accoun
|
||||
BootMode: aws.String(amiMeta.BootMode),
|
||||
}
|
||||
|
||||
if bdms := synthesizeRootBlockDeviceMapping(amiMeta); bdms != nil {
|
||||
if bdms := synthesizeRootBlockDeviceMapping(amiMeta, encryptedAtRest); bdms != nil {
|
||||
image.RootDeviceName = aws.String("/dev/sda1")
|
||||
image.BlockDeviceMappings = bdms
|
||||
}
|
||||
@@ -918,7 +919,7 @@ func (s *ImageServiceImpl) DescribeImageAttribute(input *ec2.DescribeImageAttrib
|
||||
case ec2.ImageAttributeNameDescription:
|
||||
output.Description = &ec2.AttributeValue{Value: aws.String(meta.Description)}
|
||||
case ec2.ImageAttributeNameBlockDeviceMapping:
|
||||
output.BlockDeviceMappings = synthesizeRootBlockDeviceMapping(meta)
|
||||
output.BlockDeviceMappings = synthesizeRootBlockDeviceMapping(meta, s.clusterEncryptionEnabled())
|
||||
default:
|
||||
return nil, errors.New(awserrors.ErrorInvalidParameterValue)
|
||||
}
|
||||
@@ -927,9 +928,26 @@ func (s *ImageServiceImpl) DescribeImageAttribute(input *ec2.DescribeImageAttrib
|
||||
return output, nil
|
||||
}
|
||||
|
||||
// clusterEncryptionEnabled reports whether the daemon has a viperblock master
|
||||
// key configured. AMI metadata carries no per-image encryption flag, so block
|
||||
// device synthesis falls back to this cluster-level posture.
|
||||
func (s *ImageServiceImpl) clusterEncryptionEnabled() bool {
|
||||
if s.config == nil {
|
||||
return false
|
||||
}
|
||||
mkey, err := utils.LoadViperblockMasterKey(s.config.Viperblock.EncryptionKeyFile)
|
||||
if err != nil {
|
||||
slog.Warn("clusterEncryptionEnabled: failed to load master key, reporting false", "err", err)
|
||||
return false
|
||||
}
|
||||
return mkey != nil
|
||||
}
|
||||
|
||||
// synthesizeRootBlockDeviceMapping returns /dev/sda1 with size+snapshot from
|
||||
// AMIMetadata, or nil for non-EBS AMIs.
|
||||
func synthesizeRootBlockDeviceMapping(meta viperblock.AMIMetadata) []*ec2.BlockDeviceMapping {
|
||||
// AMIMetadata, or nil for non-EBS AMIs. encrypted reflects the cluster-level
|
||||
// encryption-at-rest posture (master key configured) since AMI metadata does
|
||||
// not carry a per-image encryption flag of its own.
|
||||
func synthesizeRootBlockDeviceMapping(meta viperblock.AMIMetadata, encrypted bool) []*ec2.BlockDeviceMapping {
|
||||
if meta.RootDeviceType != "ebs" {
|
||||
return nil
|
||||
}
|
||||
@@ -937,7 +955,7 @@ func synthesizeRootBlockDeviceMapping(meta viperblock.AMIMetadata) []*ec2.BlockD
|
||||
VolumeSize: aws.Int64(utils.SafeUint64ToInt64(meta.VolumeSizeGiB)),
|
||||
VolumeType: aws.String("gp3"),
|
||||
DeleteOnTermination: aws.Bool(true),
|
||||
Encrypted: aws.Bool(false),
|
||||
Encrypted: aws.Bool(encrypted),
|
||||
}
|
||||
if meta.SnapshotID != "" {
|
||||
ebs.SnapshotId = aws.String(meta.SnapshotID)
|
||||
|
||||
@@ -280,7 +280,7 @@ func (s *SnapshotServiceImpl) CreateSnapshot(input *ec2.CreateSnapshotInput, acc
|
||||
State: "completed",
|
||||
Progress: "100%",
|
||||
StartTime: now,
|
||||
Encrypted: volumeConfig.VolumeMetadata.IsEncrypted,
|
||||
Encrypted: volumeState.EncryptionEnabled,
|
||||
OwnerID: accountID,
|
||||
AvailabilityZone: volumeConfig.VolumeMetadata.AvailabilityZone,
|
||||
Tags: utils.ExtractTags(input.TagSpecifications, "snapshot"),
|
||||
|
||||
@@ -43,7 +43,6 @@ func createTestVolume(t *testing.T, store *objectstore.MemoryObjectStore, volume
|
||||
VolumeConfig: viperblock.VolumeConfig{
|
||||
VolumeMetadata: viperblock.VolumeMetadata{
|
||||
SizeGiB: uint64(sizeGiB),
|
||||
IsEncrypted: false,
|
||||
AvailabilityZone: "us-east-1a",
|
||||
},
|
||||
},
|
||||
|
||||
@@ -161,7 +161,6 @@ func (s *VolumeServiceImpl) CreateVolume(input *ec2.CreateVolumeInput, accountID
|
||||
AvailabilityZone: *input.AvailabilityZone,
|
||||
VolumeType: volumeType,
|
||||
IOPS: iops,
|
||||
IsEncrypted: false,
|
||||
SnapshotID: snapshotID,
|
||||
},
|
||||
}
|
||||
@@ -230,7 +229,7 @@ func (s *VolumeServiceImpl) CreateVolume(input *ec2.CreateVolumeInput, accountID
|
||||
AvailabilityZone: input.AvailabilityZone,
|
||||
CreateTime: aws.Time(now),
|
||||
Iops: aws.Int64(int64(iops)),
|
||||
Encrypted: aws.Bool(false),
|
||||
Encrypted: aws.Bool(mkey != nil),
|
||||
}
|
||||
|
||||
if snapshotID != "" {
|
||||
@@ -904,7 +903,7 @@ type volumeResult struct {
|
||||
// getVolumeByID fetches a single volume's config from S3 and builds an EC2 Volume.
|
||||
// Returns the volume and the stored TenantID for account scoping.
|
||||
func (s *VolumeServiceImpl) getVolumeByID(volumeID string) (*volumeResult, error) {
|
||||
cfg, err := s.GetVolumeConfig(volumeID)
|
||||
cfg, encryptionEnabled, err := s.getVolumeConfigAndEncryption(volumeID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -937,7 +936,7 @@ func (s *VolumeServiceImpl) getVolumeByID(volumeID string) (*volumeResult, error
|
||||
AvailabilityZone: aws.String(volMeta.AvailabilityZone),
|
||||
CreateTime: aws.Time(volMeta.CreatedAt),
|
||||
VolumeType: aws.String(volumeType),
|
||||
Encrypted: aws.Bool(volMeta.IsEncrypted),
|
||||
Encrypted: aws.Bool(encryptionEnabled),
|
||||
}
|
||||
|
||||
if volMeta.IOPS > 0 {
|
||||
@@ -977,6 +976,15 @@ type volumeConfigWrapper struct {
|
||||
|
||||
// GetVolumeConfig reads the raw VolumeConfig from S3 for a given volume ID.
|
||||
func (s *VolumeServiceImpl) GetVolumeConfig(volumeID string) (*viperblock.VolumeConfig, error) {
|
||||
cfg, _, err := s.getVolumeConfigAndEncryption(volumeID)
|
||||
return cfg, err
|
||||
}
|
||||
|
||||
// getVolumeConfigAndEncryption reads config.json and surfaces both the
|
||||
// VolumeConfig and the authoritative VBState.EncryptionEnabled flag. Wrapper-
|
||||
// only blobs (pre-VBState volumes or freshly-seeded test fixtures) report
|
||||
// encryptionEnabled=false — they predate encryption-at-rest by construction.
|
||||
func (s *VolumeServiceImpl) getVolumeConfigAndEncryption(volumeID string) (*viperblock.VolumeConfig, bool, error) {
|
||||
configKey := volumeID + "/config.json"
|
||||
|
||||
getResult, err := s.store.GetObject(&s3.GetObjectInput{
|
||||
@@ -985,23 +993,31 @@ func (s *VolumeServiceImpl) GetVolumeConfig(volumeID string) (*viperblock.Volume
|
||||
})
|
||||
if err != nil {
|
||||
if objectstore.IsNoSuchKeyError(err) {
|
||||
return nil, errors.New(awserrors.ErrorInvalidVolumeNotFound)
|
||||
return nil, false, errors.New(awserrors.ErrorInvalidVolumeNotFound)
|
||||
}
|
||||
return nil, fmt.Errorf("failed to get config: %w", err)
|
||||
return nil, false, fmt.Errorf("failed to get config: %w", err)
|
||||
}
|
||||
defer getResult.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(getResult.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read config body: %w", err)
|
||||
return nil, false, fmt.Errorf("failed to read config body: %w", err)
|
||||
}
|
||||
|
||||
// Try full VBState first (matches mergeVolumeConfig's careful decode
|
||||
// pattern). A populated BlockSize is the marker that the blob is a full
|
||||
// state rather than a wrapper-only fallback.
|
||||
var state viperblock.VBState
|
||||
if decodeErr := json.NewDecoder(bytes.NewReader(body)).Decode(&state); decodeErr == nil && state.BlockSize != 0 {
|
||||
return &state.VolumeConfig, state.EncryptionEnabled, nil
|
||||
}
|
||||
|
||||
var wrapper volumeConfigWrapper
|
||||
if err := json.Unmarshal(body, &wrapper); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal config: %w", err)
|
||||
return nil, false, fmt.Errorf("failed to unmarshal config: %w", err)
|
||||
}
|
||||
|
||||
return &wrapper.VolumeConfig, nil
|
||||
return &wrapper.VolumeConfig, false, nil
|
||||
}
|
||||
|
||||
// putVolumeConfig writes a VolumeConfig back to S3 as config.json.
|
||||
|
||||
@@ -603,14 +603,29 @@ func TestGetVolumeByID_FullMetadata(t *testing.T) {
|
||||
VolumeType: "gp3",
|
||||
IOPS: 5000,
|
||||
SnapshotID: "snap-abc",
|
||||
IsEncrypted: true,
|
||||
AttachedInstance: "i-12345",
|
||||
DeviceName: "/dev/nbd0",
|
||||
DeleteOnTermination: true,
|
||||
AttachedAt: now,
|
||||
Tags: map[string]string{"Name": "test-vol", "env": "dev"},
|
||||
}
|
||||
createVolumeInStoreWithMeta(t, store, "vol-full", meta)
|
||||
// Seed as a full VBState with EncryptionEnabled=true so getVolumeByID
|
||||
// reports Encrypted via the authoritative VBState.EncryptionEnabled path.
|
||||
state := viperblock.VBState{
|
||||
VolumeName: "vol-full",
|
||||
VolumeSize: meta.SizeGiB * 1024 * 1024 * 1024,
|
||||
BlockSize: 4096,
|
||||
EncryptionEnabled: true,
|
||||
VolumeConfig: viperblock.VolumeConfig{VolumeMetadata: meta},
|
||||
}
|
||||
data, err := json.Marshal(state)
|
||||
require.NoError(t, err)
|
||||
_, err = store.PutObject(&s3.PutObjectInput{
|
||||
Bucket: aws.String("test-bucket"),
|
||||
Key: aws.String("vol-full/config.json"),
|
||||
Body: strings.NewReader(string(data)),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
result, err := svc.getVolumeByID("vol-full")
|
||||
require.NoError(t, err)
|
||||
|
||||
Reference in New Issue
Block a user