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
15 changes: 15 additions & 0 deletions api/v1beta2/awsmachine_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,21 @@ const (

// DefaultIgnitionVersion represents default Ignition version generated for machine userdata.
DefaultIgnitionVersion = "2.3"

// DefaultIgnitionStorageType represents the default storage type of Ignition userdata
DefaultIgnitionStorageType = IgnitionStorageTypeOptionClusterObjectStore

// DefaultMachinePoolIgnitionStorageType represents the default storage type of Ignition userdata for machine pools.
//
// This is only different from DefaultIgnitionStorageType because of backward compatibility. Machine pools used to
// default to store Ignition user data directly on the EC2 instance. Since the choice between remote storage (S3)
// and direct storage was introduced, the default was kept, but might change in newer API versions.
//
// GIANT SWARM CUSTOMIZED!!!: We already have clusters without explicit "ClusterObjectStore" storage type, so they
// should keep defaulting to this setting even if in the upstream PR, which merged later, the default became
// `IgnitionStorageTypeOptionUnencryptedUserData`. After upgrading all clusters to the latest cluster-aws version
// that sets the field explicitly, we can revert this to be equal to upstream's default:
DefaultMachinePoolIgnitionStorageType = IgnitionStorageTypeOptionClusterObjectStore
)

// SecretBackend defines variants for backend secret storage.
Expand Down
7 changes: 3 additions & 4 deletions api/v1beta2/awsmachine_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -399,12 +399,11 @@ func (r *AWSMachine) Default() {
}

if r.ignitionEnabled() && r.Spec.Ignition.Version == "" {
if r.Spec.Ignition == nil {
r.Spec.Ignition = &Ignition{}
}

r.Spec.Ignition.Version = DefaultIgnitionVersion
}
if r.ignitionEnabled() && r.Spec.Ignition.StorageType == "" {
r.Spec.Ignition.StorageType = DefaultIgnitionStorageType
}
}

func (r *AWSMachine) validateAdditionalSecurityGroups() field.ErrorList {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -295,8 +295,8 @@ func (t Template) ControllersPolicy() *iamv1.PolicyDocument {
Action: iamv1.Actions{
"s3:CreateBucket",
"s3:DeleteBucket",
"s3:GetObject",
"s3:DeleteObject",
"s3:GetObject",
"s3:ListBucket",
"s3:PutBucketPolicy",
"s3:PutBucketTagging",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -301,8 +301,8 @@ Resources:
- Action:
- s3:CreateBucket
- s3:DeleteBucket
- s3:GetObject
- s3:DeleteObject
- s3:GetObject
- s3:ListBucket
- s3:PutBucketPolicy
- s3:PutBucketTagging
Expand Down
6 changes: 3 additions & 3 deletions controllers/awsmachine_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -739,7 +739,7 @@ func (r *AWSMachineReconciler) resolveUserData(machineScope *scope.MachineScope,
if machineScope.UseIgnition(userDataFormat) {
var ignitionStorageType infrav1.IgnitionStorageTypeOption
if machineScope.AWSMachine.Spec.Ignition == nil {
ignitionStorageType = infrav1.IgnitionStorageTypeOptionClusterObjectStore
ignitionStorageType = infrav1.DefaultIgnitionStorageType
} else {
ignitionStorageType = machineScope.AWSMachine.Spec.Ignition.StorageType
}
Expand Down Expand Up @@ -795,8 +795,8 @@ func (r *AWSMachineReconciler) cloudInitUserData(machineScope *scope.MachineScop
// then returns the config to instruct ignition on how to pull the user data from the bucket.
func (r *AWSMachineReconciler) generateIgnitionWithRemoteStorage(scope *scope.MachineScope, objectStoreSvc services.ObjectStoreInterface, userData []byte) ([]byte, error) {
if objectStoreSvc == nil {
return nil, errors.New("using Ignition by default requires a cluster wide object storage configured at `AWSCluster.Spec.Ignition.S3Bucket`. " +
"You must configure one or instruct Ignition to use EC2 user data instead, by setting `AWSMachine.Spec.Ignition.StorageType` to `UnencryptedUserData`")
return nil, errors.New("using Ignition by default requires a cluster wide object storage configured at `AWSCluster.spec.s3Bucket`. " +
"You must configure one or instruct Ignition to use EC2 user data instead, by setting `AWSMachine.spec.ignition.storageType` to `UnencryptedUserData`")
}

objectURL, err := objectStoreSvc.Create(scope, userData)
Expand Down
3 changes: 3 additions & 0 deletions exp/api/v1beta2/awsmachinepool_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -254,4 +254,7 @@ func (r *AWSMachinePool) Default() {
if r.ignitionEnabled() && r.Spec.Ignition.Version == "" {
r.Spec.Ignition.Version = infrav1.DefaultIgnitionVersion
}
if r.ignitionEnabled() && r.Spec.Ignition.StorageType == "" {
r.Spec.Ignition.StorageType = infrav1.DefaultMachinePoolIgnitionStorageType
}
}
2 changes: 1 addition & 1 deletion exp/controllers/awsmachinepool_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -418,7 +418,7 @@ func (r *AWSMachinePoolReconciler) reconcileDelete(machinePoolScope *scope.Machi
}

launchTemplateID := machinePoolScope.AWSMachinePool.Status.LaunchTemplateID
launchTemplate, _, _, _, err := ec2Svc.GetLaunchTemplate(machinePoolScope.LaunchTemplateName())
launchTemplate, _, _, _, err := ec2Svc.GetLaunchTemplate(machinePoolScope.LaunchTemplateName()) //nolint:dogsled
if err != nil {
return err
}
Expand Down
8 changes: 7 additions & 1 deletion exp/controllers/awsmachinepool_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,14 @@ import (
"k8s.io/apimachinery/pkg/runtime"
apimachinerytypes "k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/tools/record"
utilfeature "k8s.io/component-base/featuregate/testing"
"k8s.io/klog/v2"
"k8s.io/utils/ptr"
"sigs.k8s.io/controller-runtime/pkg/client/fake"

infrav1 "sigs.k8s.io/cluster-api-provider-aws/v2/api/v1beta2"
expinfrav1 "sigs.k8s.io/cluster-api-provider-aws/v2/exp/api/v1beta2"
"sigs.k8s.io/cluster-api-provider-aws/v2/feature"
"sigs.k8s.io/cluster-api-provider-aws/v2/pkg/cloud"
"sigs.k8s.io/cluster-api-provider-aws/v2/pkg/cloud/scope"
"sigs.k8s.io/cluster-api-provider-aws/v2/pkg/cloud/services"
Expand Down Expand Up @@ -772,6 +774,8 @@ func TestAWSMachinePoolReconciler(t *testing.T) {
})

t.Run("launch template and ASG exist, bootstrap data secret name changed, Ignition bootstrap data stored in S3", func(t *testing.T) {
defer utilfeature.SetFeatureGateDuringTest(t, feature.Gates, feature.MachinePool, true)()

g := NewWithT(t)
setup(t, g)
reconciler.reconcileServiceFactory = nil // use real implementation, but keep EC2 calls mocked (`ec2ServiceFactory`)
Expand All @@ -787,7 +791,9 @@ func TestAWSMachinePoolReconciler(t *testing.T) {

// Enable Ignition S3 storage
cs.AWSCluster.Spec.S3Bucket = &infrav1.S3Bucket{}
ms.AWSMachinePool.Spec.Ignition = &infrav1.Ignition{}
ms.AWSMachinePool.Spec.Ignition = &infrav1.Ignition{
StorageType: infrav1.IgnitionStorageTypeOptionClusterObjectStore,
}
ms.AWSMachinePool.Default() // simulate webhook that sets default ignition version

asgSvc.EXPECT().GetASGByName(gomock.Any()).DoAndReturn(func(scope *scope.MachinePoolScope) (*expinfrav1.AutoScalingGroup, error) {
Expand Down
24 changes: 18 additions & 6 deletions pkg/cloud/services/ec2/launchtemplate.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,20 @@ func (s *Service) ReconcileLaunchTemplate(
return nil, err
}

var ignitionStorageType = infrav1.DefaultMachinePoolIgnitionStorageType
var ignitionVersion = infrav1.DefaultIgnitionVersion
if ignition := ignitionScope.Ignition(); ignition != nil {
ignitionStorageType = ignition.StorageType
ignitionVersion = ignition.Version
}

var userDataForLaunchTemplate []byte
if s3Scope.Bucket() != nil && bootstrapDataFormat == "ignition" && ignitionScope.Ignition() != nil {
if bootstrapDataFormat == "ignition" && ignitionStorageType == infrav1.IgnitionStorageTypeOptionClusterObjectStore {
if s3Scope.Bucket() == nil {
return nil, errors.New("using Ignition with `AWSMachinePool.spec.ignition.storageType=ClusterObjectStore` " +
"requires a cluster wide object storage configured at `AWSCluster.spec.s3Bucket`")
}

scope.Info("Using S3 bucket storage for Ignition format")

// S3 bucket storage enabled and Ignition format is used. Ignition supports reading large user data from S3,
Expand All @@ -108,10 +120,9 @@ func (s *Service) ReconcileLaunchTemplate(
return nil, err
}

ignVersion := ignitionScope.Ignition().Version
semver, err := semver.ParseTolerant(ignVersion)
semver, err := semver.ParseTolerant(ignitionVersion)
if err != nil {
err = errors.Wrapf(err, "failed to parse ignition version %q", ignVersion)
err = errors.Wrapf(err, "failed to parse ignition version %q", ignitionVersion)
conditions.MarkFalse(scope.GetSetter(), expinfrav1.LaunchTemplateReadyCondition, expinfrav1.LaunchTemplateReconcileFailedReason, clusterv1.ConditionSeverityError, err.Error())
return nil, err
}
Expand Down Expand Up @@ -159,7 +170,7 @@ func (s *Service) ReconcileLaunchTemplate(
return nil, err
}
default:
err = errors.Errorf("unsupported ignition version %q", ignVersion)
err = errors.Errorf("unsupported ignition version %q", ignitionVersion)
conditions.MarkFalse(scope.GetSetter(), expinfrav1.LaunchTemplateReadyCondition, expinfrav1.LaunchTemplateReconcileFailedReason, clusterv1.ConditionSeverityError, err.Error())
return nil, err
}
Expand Down Expand Up @@ -279,7 +290,8 @@ func (s *Service) ReconcileLaunchTemplate(
if err != nil {
return nil, err
}
if deletedLaunchTemplateVersionBootstrapDataHash != nil && s3Scope.Bucket() != nil && bootstrapDataFormat == "ignition" && ignitionScope.Ignition() != nil {

if deletedLaunchTemplateVersionBootstrapDataHash != nil && s3Scope.Bucket() != nil && bootstrapDataFormat == "ignition" && ignitionStorageType == infrav1.IgnitionStorageTypeOptionClusterObjectStore {
scope.Info("Deleting S3 object for deleted launch template version", "version", *deletedLaunchTemplateVersion.VersionNumber)

err = objectStoreSvc.DeleteForMachinePool(scope, *deletedLaunchTemplateVersionBootstrapDataHash)
Expand Down
18 changes: 10 additions & 8 deletions pkg/cloud/services/ec2/launchtemplate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,14 +85,16 @@ var testBootstrapDataHash = userdata.ComputeHash(testBootstrapData)

func defaultEC2AndDataTags(name string, clusterName string, userDataSecretKey types.NamespacedName, bootstrapDataHash string) []*ec2.Tag {
tags := defaultEC2Tags(name, clusterName)
tags = append(tags, &ec2.Tag{
Key: aws.String(infrav1.LaunchTemplateBootstrapDataSecret),
Value: aws.String(userDataSecretKey.String()),
})
tags = append(tags, &ec2.Tag{
Key: aws.String(infrav1.LaunchTemplateBootstrapDataHash),
Value: aws.String(bootstrapDataHash),
})
tags = append(
tags,
&ec2.Tag{
Key: aws.String(infrav1.LaunchTemplateBootstrapDataSecret),
Value: aws.String(userDataSecretKey.String()),
},
&ec2.Tag{
Key: aws.String(infrav1.LaunchTemplateBootstrapDataHash),
Value: aws.String(bootstrapDataHash),
})
sortTags(tags)
return tags
}
Expand Down
40 changes: 23 additions & 17 deletions pkg/cloud/services/s3/s3.go
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ func (s *Service) Create(m *scope.MachineScope, data []byte) (string, error) {
return objectURL.String(), nil
}

// CreateForMachinePool creates an object for machine pool related bootstrap data in the S3 bucket.
func (s *Service) CreateForMachinePool(scope scope.LaunchTemplateScope, data []byte) (string, error) {
if !s.bucketManagementEnabled() {
return "", errors.New("requested object creation but bucket management is not enabled")
Expand Down Expand Up @@ -336,6 +337,7 @@ func (s *Service) deleteObject(bucket, key string) error {
return nil
}

// DeleteForMachinePool deletes the object for machine pool related bootstrap data from the S3 bucket.
func (s *Service) DeleteForMachinePool(scope scope.LaunchTemplateScope, bootstrapDataHash string) error {
if !s.bucketManagementEnabled() {
return errors.New("requested object deletion but bucket management is not enabled")
Expand Down Expand Up @@ -440,6 +442,8 @@ func (s *Service) ensureBucketLifecycleConfiguration(bucketName string) error {
// The bootstrap token for new nodes to join the cluster is normally rotated regularly,
// such as in CAPI's `KubeadmConfig` reconciler. Therefore, the launch template user data
// stored in the S3 bucket only needs to live longer than the token TTL.
// This lifecycle policy is here as backup. Normally, CAPA should delete outdated S3 objects
// (see function `DeleteForMachinePool`).
Days: aws.Int64(1),
},
Filter: &s3.LifecycleRuleFilter{
Expand Down Expand Up @@ -537,24 +541,26 @@ func (s *Service) bucketPolicy(bucketName string) (string, error) {
}

for _, iamInstanceProfile := range bucket.NodesIAMInstanceProfiles {
statements = append(statements, iam.StatementEntry{
Sid: iamInstanceProfile,
Effect: iam.EffectAllow,
Principal: map[iam.PrincipalType]iam.PrincipalID{
iam.PrincipalAWS: []string{fmt.Sprintf("arn:%s:iam::%s:role/%s", partition, *accountID.Account, iamInstanceProfile)},
},
Action: []string{"s3:GetObject"},
Resource: []string{fmt.Sprintf("arn:%s:s3:::%s/node/*", partition, bucketName)},
})
statements = append(statements, iam.StatementEntry{
Sid: iamInstanceProfile,
Effect: iam.EffectAllow,
Principal: map[iam.PrincipalType]iam.PrincipalID{
iam.PrincipalAWS: []string{fmt.Sprintf("arn:%s:iam::%s:role/%s", partition, *accountID.Account, iamInstanceProfile)},
statements = append(
statements,
iam.StatementEntry{
Sid: iamInstanceProfile,
Effect: iam.EffectAllow,
Principal: map[iam.PrincipalType]iam.PrincipalID{
iam.PrincipalAWS: []string{fmt.Sprintf("arn:%s:iam::%s:role/%s", partition, *accountID.Account, iamInstanceProfile)},
},
Action: []string{"s3:GetObject"},
Resource: []string{fmt.Sprintf("arn:%s:s3:::%s/node/*", partition, bucketName)},
},
Action: []string{"s3:GetObject"},
Resource: []string{fmt.Sprintf("arn:%s:s3:::%s/machine-pool/*", partition, bucketName)},
})
iam.StatementEntry{
Sid: iamInstanceProfile,
Effect: iam.EffectAllow,
Principal: map[iam.PrincipalType]iam.PrincipalID{
iam.PrincipalAWS: []string{fmt.Sprintf("arn:%s:iam::%s:role/%s", partition, *accountID.Account, iamInstanceProfile)},
},
Action: []string{"s3:GetObject"},
Resource: []string{fmt.Sprintf("arn:%s:s3:::%s/machine-pool/*", partition, bucketName)},
})
}
}

Expand Down
Loading