diff --git a/api/v1beta1/awscluster_conversion.go b/api/v1beta1/awscluster_conversion.go index abd5b940f3..a327dc70dc 100644 --- a/api/v1beta1/awscluster_conversion.go +++ b/api/v1beta1/awscluster_conversion.go @@ -44,6 +44,9 @@ func (src *AWSCluster) ConvertTo(dstRaw conversion.Hub) error { restoreControlPlaneLoadBalancerStatus(&restored.Status.Network.APIServerELB, &dst.Status.Network.APIServerELB) dst.Spec.S3Bucket = restored.Spec.S3Bucket + if restored.Status.Bastion != nil { + dst.Status.Bastion.InstanceMetadataOptions = restored.Status.Bastion.InstanceMetadataOptions + } return nil } diff --git a/api/v1beta1/awsmachine_conversion.go b/api/v1beta1/awsmachine_conversion.go index 2e36f50671..2fa13af817 100644 --- a/api/v1beta1/awsmachine_conversion.go +++ b/api/v1beta1/awsmachine_conversion.go @@ -36,6 +36,7 @@ func (src *AWSMachine) ConvertTo(dstRaw conversion.Hub) error { } dst.Spec.Ignition = restored.Spec.Ignition + dst.Spec.InstanceMetadataOptions = restored.Spec.InstanceMetadataOptions return nil } @@ -81,6 +82,7 @@ func (r *AWSMachineTemplate) ConvertTo(dstRaw conversion.Hub) error { dst.Spec.Template.ObjectMeta = restored.Spec.Template.ObjectMeta dst.Spec.Template.Spec.Ignition = restored.Spec.Template.Spec.Ignition + dst.Spec.Template.Spec.InstanceMetadataOptions = restored.Spec.Template.Spec.InstanceMetadataOptions return nil } diff --git a/api/v1beta1/conversion.go b/api/v1beta1/conversion.go index c8c2789c38..a8cf22f98b 100644 --- a/api/v1beta1/conversion.go +++ b/api/v1beta1/conversion.go @@ -43,6 +43,14 @@ func Convert_v1beta2_NetworkStatus_To_v1beta1_NetworkStatus(in *v1beta2.NetworkS return autoConvert_v1beta2_NetworkStatus_To_v1beta1_NetworkStatus(in, out, s) } +func Convert_v1beta2_AWSMachineSpec_To_v1beta1_AWSMachineSpec(in *v1beta2.AWSMachineSpec, out *AWSMachineSpec, s conversion.Scope) error { + return autoConvert_v1beta2_AWSMachineSpec_To_v1beta1_AWSMachineSpec(in, out, s) +} + +func Convert_v1beta2_Instance_To_v1beta1_Instance(in *v1beta2.Instance, out *Instance, s conversion.Scope) error { + return autoConvert_v1beta2_Instance_To_v1beta1_Instance(in, out, s) +} + func Convert_v1beta1_ClassicELB_To_v1beta2_LoadBalancer(in *ClassicELB, out *v1beta2.LoadBalancer, s conversion.Scope) error { out.Name = in.Name out.DNSName = in.DNSName diff --git a/api/v1beta1/zz_generated.conversion.go b/api/v1beta1/zz_generated.conversion.go index 2fb0fe3066..01aca175ed 100644 --- a/api/v1beta1/zz_generated.conversion.go +++ b/api/v1beta1/zz_generated.conversion.go @@ -260,11 +260,6 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } - if err := s.AddGeneratedConversionFunc((*v1beta2.AWSMachineSpec)(nil), (*AWSMachineSpec)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_v1beta2_AWSMachineSpec_To_v1beta1_AWSMachineSpec(a.(*v1beta2.AWSMachineSpec), b.(*AWSMachineSpec), scope) - }); err != nil { - return err - } if err := s.AddGeneratedConversionFunc((*AWSMachineStatus)(nil), (*v1beta2.AWSMachineStatus)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1beta1_AWSMachineStatus_To_v1beta2_AWSMachineStatus(a.(*AWSMachineStatus), b.(*v1beta2.AWSMachineStatus), scope) }); err != nil { @@ -475,11 +470,6 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } - if err := s.AddGeneratedConversionFunc((*v1beta2.Instance)(nil), (*Instance)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_v1beta2_Instance_To_v1beta1_Instance(a.(*v1beta2.Instance), b.(*Instance), scope) - }); err != nil { - return err - } if err := s.AddGeneratedConversionFunc((*NetworkSpec)(nil), (*v1beta2.NetworkSpec)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1beta1_NetworkSpec_To_v1beta2_NetworkSpec(a.(*NetworkSpec), b.(*v1beta2.NetworkSpec), scope) }); err != nil { @@ -590,6 +580,16 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } + if err := s.AddConversionFunc((*v1beta2.AWSMachineSpec)(nil), (*AWSMachineSpec)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1beta2_AWSMachineSpec_To_v1beta1_AWSMachineSpec(a.(*v1beta2.AWSMachineSpec), b.(*AWSMachineSpec), scope) + }); err != nil { + return err + } + if err := s.AddConversionFunc((*v1beta2.Instance)(nil), (*Instance)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1beta2_Instance_To_v1beta1_Instance(a.(*v1beta2.Instance), b.(*Instance), scope) + }); err != nil { + return err + } if err := s.AddConversionFunc((*v1beta2.LoadBalancer)(nil), (*ClassicELB)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1beta2_LoadBalancer_To_v1beta1_ClassicELB(a.(*v1beta2.LoadBalancer), b.(*ClassicELB), scope) }); err != nil { @@ -1014,7 +1014,15 @@ func autoConvert_v1beta1_AWSClusterStatus_To_v1beta2_AWSClusterStatus(in *AWSClu return err } out.FailureDomains = *(*apiv1beta1.FailureDomains)(unsafe.Pointer(&in.FailureDomains)) - out.Bastion = (*v1beta2.Instance)(unsafe.Pointer(in.Bastion)) + if in.Bastion != nil { + in, out := &in.Bastion, &out.Bastion + *out = new(v1beta2.Instance) + if err := Convert_v1beta1_Instance_To_v1beta2_Instance(*in, *out, s); err != nil { + return err + } + } else { + out.Bastion = nil + } out.Conditions = *(*apiv1beta1.Conditions)(unsafe.Pointer(&in.Conditions)) return nil } @@ -1030,7 +1038,15 @@ func autoConvert_v1beta2_AWSClusterStatus_To_v1beta1_AWSClusterStatus(in *v1beta return err } out.FailureDomains = *(*apiv1beta1.FailureDomains)(unsafe.Pointer(&in.FailureDomains)) - out.Bastion = (*Instance)(unsafe.Pointer(in.Bastion)) + if in.Bastion != nil { + in, out := &in.Bastion, &out.Bastion + *out = new(Instance) + if err := Convert_v1beta2_Instance_To_v1beta1_Instance(*in, *out, s); err != nil { + return err + } + } else { + out.Bastion = nil + } out.Conditions = *(*apiv1beta1.Conditions)(unsafe.Pointer(&in.Conditions)) return nil } @@ -1333,6 +1349,7 @@ func autoConvert_v1beta1_AWSMachineSpec_To_v1beta2_AWSMachineSpec(in *AWSMachine func autoConvert_v1beta2_AWSMachineSpec_To_v1beta1_AWSMachineSpec(in *v1beta2.AWSMachineSpec, out *AWSMachineSpec, s conversion.Scope) error { out.ProviderID = (*string)(unsafe.Pointer(in.ProviderID)) out.InstanceID = (*string)(unsafe.Pointer(in.InstanceID)) + // WARNING: in.InstanceMetadataOptions requires manual conversion: does not exist in peer-type if err := Convert_v1beta2_AMIReference_To_v1beta1_AMIReference(&in.AMI, &out.AMI, s); err != nil { return err } @@ -1377,11 +1394,6 @@ func autoConvert_v1beta2_AWSMachineSpec_To_v1beta1_AWSMachineSpec(in *v1beta2.AW return nil } -// Convert_v1beta2_AWSMachineSpec_To_v1beta1_AWSMachineSpec is an autogenerated conversion function. -func Convert_v1beta2_AWSMachineSpec_To_v1beta1_AWSMachineSpec(in *v1beta2.AWSMachineSpec, out *AWSMachineSpec, s conversion.Scope) error { - return autoConvert_v1beta2_AWSMachineSpec_To_v1beta1_AWSMachineSpec(in, out, s) -} - func autoConvert_v1beta1_AWSMachineStatus_To_v1beta2_AWSMachineStatus(in *AWSMachineStatus, out *v1beta2.AWSMachineStatus, s conversion.Scope) error { out.Ready = in.Ready out.Interruptible = in.Interruptible @@ -1984,14 +1996,10 @@ func autoConvert_v1beta2_Instance_To_v1beta1_Instance(in *v1beta2.Instance, out out.SpotMarketOptions = (*SpotMarketOptions)(unsafe.Pointer(in.SpotMarketOptions)) out.Tenancy = in.Tenancy out.VolumeIDs = *(*[]string)(unsafe.Pointer(&in.VolumeIDs)) + // WARNING: in.InstanceMetadataOptions requires manual conversion: does not exist in peer-type return nil } -// Convert_v1beta2_Instance_To_v1beta1_Instance is an autogenerated conversion function. -func Convert_v1beta2_Instance_To_v1beta1_Instance(in *v1beta2.Instance, out *Instance, s conversion.Scope) error { - return autoConvert_v1beta2_Instance_To_v1beta1_Instance(in, out, s) -} - func autoConvert_v1beta1_NetworkSpec_To_v1beta2_NetworkSpec(in *NetworkSpec, out *v1beta2.NetworkSpec, s conversion.Scope) error { if err := Convert_v1beta1_VPCSpec_To_v1beta2_VPCSpec(&in.VPC, &out.VPC, s); err != nil { return err diff --git a/api/v1beta2/awsmachine_types.go b/api/v1beta2/awsmachine_types.go index 5baf4fca0f..b995eb2255 100644 --- a/api/v1beta2/awsmachine_types.go +++ b/api/v1beta2/awsmachine_types.go @@ -51,6 +51,10 @@ type AWSMachineSpec struct { // InstanceID is the EC2 instance ID for this machine. InstanceID *string `json:"instanceID,omitempty"` + // InstanceMetadataOptions is the metadata options for the EC2 instance. + // +optional + InstanceMetadataOptions *InstanceMetadataOptions `json:"instanceMetadataOptions,omitempty"` + // AMI is the reference to the AMI from which to create the machine instance. AMI AMIReference `json:"ami,omitempty"` diff --git a/api/v1beta2/defaults.go b/api/v1beta2/defaults.go index ff24f92574..012fd3fc5c 100644 --- a/api/v1beta2/defaults.go +++ b/api/v1beta2/defaults.go @@ -79,3 +79,15 @@ func SetDefaults_Labels(obj *metav1.ObjectMeta) { //nolint:golint,stylecheck clusterv1.ClusterctlMoveHierarchyLabelName: ""} } } + +// SetDefaults_AWSMachineSpec is used by defaulter-gen. +func SetDefaults_AWSMachineSpec(obj *AWSMachineSpec) { //nolint:golint,stylecheck + if obj.InstanceMetadataOptions == nil { + obj.InstanceMetadataOptions = &InstanceMetadataOptions{ + HTTPEndpoint: InstanceMetadataEndpointStateEnabled, + HTTPPutResponseHopLimit: 1, + HTTPTokens: HTTPTokensStateRequired, // Defaults to IMDSv2 + InstanceMetadataTags: InstanceMetadataEndpointStateDisabled, + } + } +} diff --git a/api/v1beta2/types.go b/api/v1beta2/types.go index a9e3d357a6..71a6edf563 100644 --- a/api/v1beta2/types.go +++ b/api/v1beta2/types.go @@ -201,6 +201,83 @@ type Instance struct { // IDs of the instance's volumes // +optional VolumeIDs []string `json:"volumeIDs,omitempty"` + + // InstanceMetadataOptions is the metadata options for the EC2 instance. + // +optional + InstanceMetadataOptions *InstanceMetadataOptions `json:"instanceMetadataOptions,omitempty"` +} + +// InstanceMetadataState describes the state of InstanceMetadataOptions.HttpEndpoint and InstanceMetadataOptions.InstanceMetadataTags +type InstanceMetadataState string + +const ( + // InstanceMetadataEndpointStateDisabled represents the disabled state + InstanceMetadataEndpointStateDisabled = InstanceMetadataState("disabled") + + // InstanceMetadataEndpointStateEnabled represents the enabled state + InstanceMetadataEndpointStateEnabled = InstanceMetadataState("enabled") +) + +// HTTPTokensState describes the state of InstanceMetadataOptions.HTTPTokensState +type HTTPTokensState string + +const ( + // HTTPTokensStateOptional represents the optional state + HTTPTokensStateOptional = HTTPTokensState("optional") + + // HTTPTokensStateRequired represents the required state (IMDSv2) + HTTPTokensStateRequired = HTTPTokensState("required") +) + +// InstanceMetadataOptions describes metadata options for the EC2 instance. +type InstanceMetadataOptions struct { + // Enables or disables the HTTP metadata endpoint on your instances. + // + // If you specify a value of disabled, you cannot access your instance metadata. + // + // Default: enabled + // + // +kubebuilder:validation:Enum:=enabled;disabled + // +kubebuilder:default=enabled + HTTPEndpoint InstanceMetadataState `json:"httpEndpoint,omitempty"` + + // The desired HTTP PUT response hop limit for instance metadata requests. The + // larger the number, the further instance metadata requests can travel. + // + // Default: 1 + // + // +kubebuilder:validation:Minimum:=1 + // +kubebuilder:validation:Maximum:=64 + // +kubebuilder:default=1 + HTTPPutResponseHopLimit int64 `json:"httpPutResponseHopLimit,omitempty"` + + // The state of token usage for your instance metadata requests. + // + // If the state is optional, you can choose to retrieve instance metadata with + // or without a session token on your request. If you retrieve the IAM role + // credentials without a token, the version 1.0 role credentials are returned. + // If you retrieve the IAM role credentials using a valid session token, the + // version 2.0 role credentials are returned. + // + // If the state is required, you must send a session token with any instance + // metadata retrieval requests. In this state, retrieving the IAM role credentials + // always returns the version 2.0 credentials; the version 1.0 credentials are + // not available. + // + // Default: required + // +kubebuilder:validation:Enum:=optional;required + // +kubebuilder:default=required + HTTPTokens HTTPTokensState `json:"httpTokens,omitempty"` + + // Set to enabled to allow access to instance tags from the instance metadata. + // Set to disabled to turn off access to instance tags from the instance metadata. + // For more information, see Work with instance tags using the instance metadata + // (https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/Using_Tags.html#work-with-tags-in-IMDS). + // + // Default: disabled + // +kubebuilder:validation:Enum:=enabled;disabled + // +kubebuilder:default=disabled + InstanceMetadataTags InstanceMetadataState `json:"instanceMetadataTags,omitempty"` } // Volume encapsulates the configuration options for the storage device. diff --git a/api/v1beta2/zz_generated.deepcopy.go b/api/v1beta2/zz_generated.deepcopy.go index 199f547c5e..111a21f199 100644 --- a/api/v1beta2/zz_generated.deepcopy.go +++ b/api/v1beta2/zz_generated.deepcopy.go @@ -658,6 +658,11 @@ func (in *AWSMachineSpec) DeepCopyInto(out *AWSMachineSpec) { *out = new(string) **out = **in } + if in.InstanceMetadataOptions != nil { + in, out := &in.InstanceMetadataOptions, &out.InstanceMetadataOptions + *out = new(InstanceMetadataOptions) + **out = **in + } in.AMI.DeepCopyInto(&out.AMI) if in.AdditionalTags != nil { in, out := &in.AdditionalTags, &out.AdditionalTags @@ -1403,6 +1408,11 @@ func (in *Instance) DeepCopyInto(out *Instance) { *out = make([]string, len(*in)) copy(*out, *in) } + if in.InstanceMetadataOptions != nil { + in, out := &in.InstanceMetadataOptions, &out.InstanceMetadataOptions + *out = new(InstanceMetadataOptions) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Instance. @@ -1415,6 +1425,21 @@ func (in *Instance) DeepCopy() *Instance { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *InstanceMetadataOptions) DeepCopyInto(out *InstanceMetadataOptions) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InstanceMetadataOptions. +func (in *InstanceMetadataOptions) DeepCopy() *InstanceMetadataOptions { + if in == nil { + return nil + } + out := new(InstanceMetadataOptions) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Listener) DeepCopyInto(out *Listener) { *out = *in diff --git a/api/v1beta2/zz_generated.defaults.go b/api/v1beta2/zz_generated.defaults.go index 6deb602064..506e7e7805 100644 --- a/api/v1beta2/zz_generated.defaults.go +++ b/api/v1beta2/zz_generated.defaults.go @@ -31,6 +31,8 @@ import ( func RegisterDefaults(scheme *runtime.Scheme) error { scheme.AddTypeDefaultingFunc(&AWSCluster{}, func(obj interface{}) { SetObjectDefaults_AWSCluster(obj.(*AWSCluster)) }) scheme.AddTypeDefaultingFunc(&AWSClusterTemplate{}, func(obj interface{}) { SetObjectDefaults_AWSClusterTemplate(obj.(*AWSClusterTemplate)) }) + scheme.AddTypeDefaultingFunc(&AWSMachine{}, func(obj interface{}) { SetObjectDefaults_AWSMachine(obj.(*AWSMachine)) }) + scheme.AddTypeDefaultingFunc(&AWSMachineTemplate{}, func(obj interface{}) { SetObjectDefaults_AWSMachineTemplate(obj.(*AWSMachineTemplate)) }) return nil } @@ -45,3 +47,11 @@ func SetObjectDefaults_AWSClusterTemplate(in *AWSClusterTemplate) { SetDefaults_NetworkSpec(&in.Spec.Template.Spec.NetworkSpec) SetDefaults_Bastion(&in.Spec.Template.Spec.Bastion) } + +func SetObjectDefaults_AWSMachine(in *AWSMachine) { + SetDefaults_AWSMachineSpec(&in.Spec) +} + +func SetObjectDefaults_AWSMachineTemplate(in *AWSMachineTemplate) { + SetDefaults_AWSMachineSpec(&in.Spec.Template.Spec) +} diff --git a/config/crd/bases/controlplane.cluster.x-k8s.io_awsmanagedcontrolplanes.yaml b/config/crd/bases/controlplane.cluster.x-k8s.io_awsmanagedcontrolplanes.yaml index 9612e40ac0..041d8cf479 100644 --- a/config/crd/bases/controlplane.cluster.x-k8s.io_awsmanagedcontrolplanes.yaml +++ b/config/crd/bases/controlplane.cluster.x-k8s.io_awsmanagedcontrolplanes.yaml @@ -843,6 +843,59 @@ spec: imageId: description: The ID of the AMI used to launch the instance. type: string + instanceMetadataOptions: + description: InstanceMetadataOptions is the metadata options for + the EC2 instance. + properties: + httpEndpoint: + default: enabled + description: "Enables or disables the HTTP metadata endpoint + on your instances. \n If you specify a value of disabled, + you cannot access your instance metadata. \n Default: enabled" + enum: + - enabled + - disabled + type: string + httpPutResponseHopLimit: + default: 1 + description: "The desired HTTP PUT response hop limit for + instance metadata requests. The larger the number, the further + instance metadata requests can travel. \n Default: 1" + format: int64 + maximum: 64 + minimum: 1 + type: integer + httpTokens: + default: required + description: "The state of token usage for your instance metadata + requests. \n If the state is optional, you can choose to + retrieve instance metadata with or without a session token + on your request. If you retrieve the IAM role credentials + without a token, the version 1.0 role credentials are returned. + If you retrieve the IAM role credentials using a valid session + token, the version 2.0 role credentials are returned. \n + If the state is required, you must send a session token + with any instance metadata retrieval requests. In this state, + retrieving the IAM role credentials always returns the version + 2.0 credentials; the version 1.0 credentials are not available. + \n Default: required" + enum: + - optional + - required + type: string + instanceMetadataTags: + default: disabled + description: "Set to enabled to allow access to instance tags + from the instance metadata. Set to disabled to turn off + access to instance tags from the instance metadata. For + more information, see Work with instance tags using the + instance metadata (https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/Using_Tags.html#work-with-tags-in-IMDS). + \n Default: disabled" + enum: + - enabled + - disabled + type: string + type: object instanceState: description: The current state of the instance. type: string @@ -2208,6 +2261,59 @@ spec: imageId: description: The ID of the AMI used to launch the instance. type: string + instanceMetadataOptions: + description: InstanceMetadataOptions is the metadata options for + the EC2 instance. + properties: + httpEndpoint: + default: enabled + description: "Enables or disables the HTTP metadata endpoint + on your instances. \n If you specify a value of disabled, + you cannot access your instance metadata. \n Default: enabled" + enum: + - enabled + - disabled + type: string + httpPutResponseHopLimit: + default: 1 + description: "The desired HTTP PUT response hop limit for + instance metadata requests. The larger the number, the further + instance metadata requests can travel. \n Default: 1" + format: int64 + maximum: 64 + minimum: 1 + type: integer + httpTokens: + default: required + description: "The state of token usage for your instance metadata + requests. \n If the state is optional, you can choose to + retrieve instance metadata with or without a session token + on your request. If you retrieve the IAM role credentials + without a token, the version 1.0 role credentials are returned. + If you retrieve the IAM role credentials using a valid session + token, the version 2.0 role credentials are returned. \n + If the state is required, you must send a session token + with any instance metadata retrieval requests. In this state, + retrieving the IAM role credentials always returns the version + 2.0 credentials; the version 1.0 credentials are not available. + \n Default: required" + enum: + - optional + - required + type: string + instanceMetadataTags: + default: disabled + description: "Set to enabled to allow access to instance tags + from the instance metadata. Set to disabled to turn off + access to instance tags from the instance metadata. For + more information, see Work with instance tags using the + instance metadata (https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/Using_Tags.html#work-with-tags-in-IMDS). + \n Default: disabled" + enum: + - enabled + - disabled + type: string + type: object instanceState: description: The current state of the instance. type: string diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsclusters.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsclusters.yaml index 32941aba6e..5a534e5598 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsclusters.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsclusters.yaml @@ -1324,6 +1324,59 @@ spec: imageId: description: The ID of the AMI used to launch the instance. type: string + instanceMetadataOptions: + description: InstanceMetadataOptions is the metadata options for + the EC2 instance. + properties: + httpEndpoint: + default: enabled + description: "Enables or disables the HTTP metadata endpoint + on your instances. \n If you specify a value of disabled, + you cannot access your instance metadata. \n Default: enabled" + enum: + - enabled + - disabled + type: string + httpPutResponseHopLimit: + default: 1 + description: "The desired HTTP PUT response hop limit for + instance metadata requests. The larger the number, the further + instance metadata requests can travel. \n Default: 1" + format: int64 + maximum: 64 + minimum: 1 + type: integer + httpTokens: + default: required + description: "The state of token usage for your instance metadata + requests. \n If the state is optional, you can choose to + retrieve instance metadata with or without a session token + on your request. If you retrieve the IAM role credentials + without a token, the version 1.0 role credentials are returned. + If you retrieve the IAM role credentials using a valid session + token, the version 2.0 role credentials are returned. \n + If the state is required, you must send a session token + with any instance metadata retrieval requests. In this state, + retrieving the IAM role credentials always returns the version + 2.0 credentials; the version 1.0 credentials are not available. + \n Default: required" + enum: + - optional + - required + type: string + instanceMetadataTags: + default: disabled + description: "Set to enabled to allow access to instance tags + from the instance metadata. Set to disabled to turn off + access to instance tags from the instance metadata. For + more information, see Work with instance tags using the + instance metadata (https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/Using_Tags.html#work-with-tags-in-IMDS). + \n Default: disabled" + enum: + - enabled + - disabled + type: string + type: object instanceState: description: The current state of the instance. type: string diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachines.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachines.yaml index 29e50c64e2..2c71b24ad6 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachines.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachines.yaml @@ -665,6 +665,57 @@ spec: instanceID: description: InstanceID is the EC2 instance ID for this machine. type: string + instanceMetadataOptions: + description: InstanceMetadataOptions is the metadata options for the + EC2 instance. + properties: + httpEndpoint: + default: enabled + description: "Enables or disables the HTTP metadata endpoint on + your instances. \n If you specify a value of disabled, you cannot + access your instance metadata. \n Default: enabled" + enum: + - enabled + - disabled + type: string + httpPutResponseHopLimit: + default: 1 + description: "The desired HTTP PUT response hop limit for instance + metadata requests. The larger the number, the further instance + metadata requests can travel. \n Default: 1" + format: int64 + maximum: 64 + minimum: 1 + type: integer + httpTokens: + default: required + description: "The state of token usage for your instance metadata + requests. \n If the state is optional, you can choose to retrieve + instance metadata with or without a session token on your request. + If you retrieve the IAM role credentials without a token, the + version 1.0 role credentials are returned. If you retrieve the + IAM role credentials using a valid session token, the version + 2.0 role credentials are returned. \n If the state is required, + you must send a session token with any instance metadata retrieval + requests. In this state, retrieving the IAM role credentials + always returns the version 2.0 credentials; the version 1.0 + credentials are not available. \n Default: required" + enum: + - optional + - required + type: string + instanceMetadataTags: + default: disabled + description: "Set to enabled to allow access to instance tags + from the instance metadata. Set to disabled to turn off access + to instance tags from the instance metadata. For more information, + see Work with instance tags using the instance metadata (https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/Using_Tags.html#work-with-tags-in-IMDS). + \n Default: disabled" + enum: + - enabled + - disabled + type: string + type: object instanceType: description: 'InstanceType is the type of instance to create. Example: m4.xlarge' diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachinetemplates.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachinetemplates.yaml index 0c1829a74f..7b7aef383d 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachinetemplates.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachinetemplates.yaml @@ -612,6 +612,62 @@ spec: instanceID: description: InstanceID is the EC2 instance ID for this machine. type: string + instanceMetadataOptions: + description: InstanceMetadataOptions is the metadata options + for the EC2 instance. + properties: + httpEndpoint: + default: enabled + description: "Enables or disables the HTTP metadata endpoint + on your instances. \n If you specify a value of disabled, + you cannot access your instance metadata. \n Default: + enabled" + enum: + - enabled + - disabled + type: string + httpPutResponseHopLimit: + default: 1 + description: "The desired HTTP PUT response hop limit + for instance metadata requests. The larger the number, + the further instance metadata requests can travel. \n + Default: 1" + format: int64 + maximum: 64 + minimum: 1 + type: integer + httpTokens: + default: required + description: "The state of token usage for your instance + metadata requests. \n If the state is optional, you + can choose to retrieve instance metadata with or without + a session token on your request. If you retrieve the + IAM role credentials without a token, the version 1.0 + role credentials are returned. If you retrieve the IAM + role credentials using a valid session token, the version + 2.0 role credentials are returned. \n If the state is + required, you must send a session token with any instance + metadata retrieval requests. In this state, retrieving + the IAM role credentials always returns the version + 2.0 credentials; the version 1.0 credentials are not + available. \n Default: required" + enum: + - optional + - required + type: string + instanceMetadataTags: + default: disabled + description: "Set to enabled to allow access to instance + tags from the instance metadata. Set to disabled to + turn off access to instance tags from the instance metadata. + For more information, see Work with instance tags using + the instance metadata (https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/Using_Tags.html#work-with-tags-in-IMDS). + \n Default: disabled" + enum: + - enabled + - disabled + type: string + type: object instanceType: description: 'InstanceType is the type of instance to create. Example: m4.xlarge' diff --git a/controllers/awsmachine_controller.go b/controllers/awsmachine_controller.go index a18f728238..4e1ea70527 100644 --- a/controllers/awsmachine_controller.go +++ b/controllers/awsmachine_controller.go @@ -585,28 +585,43 @@ func (r *AWSMachineReconciler) reconcileNormal(_ context.Context, machineScope * // tasks that can only take place during operational instance states if machineScope.InstanceIsOperational() { - machineScope.SetAddresses(instance.Addresses) - - existingSecurityGroups, err := ec2svc.GetInstanceSecurityGroups(*machineScope.GetInstanceID()) + err := r.reconcileOperationalState(ec2svc, machineScope, instance) if err != nil { - machineScope.Error(err, "unable to get instance security groups") return ctrl.Result{}, err } - - // Ensure that the security groups are correct. - _, err = r.ensureSecurityGroups(ec2svc, machineScope, machineScope.AWSMachine.Spec.AdditionalSecurityGroups, existingSecurityGroups) - if err != nil { - conditions.MarkFalse(machineScope.AWSMachine, infrav1.SecurityGroupsReadyCondition, infrav1.SecurityGroupsFailedReason, clusterv1.ConditionSeverityError, err.Error()) - machineScope.Error(err, "unable to ensure security groups") - return ctrl.Result{}, err - } - conditions.MarkTrue(machineScope.AWSMachine, infrav1.SecurityGroupsReadyCondition) } machineScope.Debug("done reconciling instance", "instance", instance) return ctrl.Result{}, nil } +func (r *AWSMachineReconciler) reconcileOperationalState(ec2svc services.EC2Interface, machineScope *scope.MachineScope, instance *infrav1.Instance) error { + machineScope.SetAddresses(instance.Addresses) + + existingSecurityGroups, err := ec2svc.GetInstanceSecurityGroups(*machineScope.GetInstanceID()) + if err != nil { + machineScope.Error(err, "unable to get instance security groups") + return err + } + + // Ensure that the security groups are correct. + _, err = r.ensureSecurityGroups(ec2svc, machineScope, machineScope.AWSMachine.Spec.AdditionalSecurityGroups, existingSecurityGroups) + if err != nil { + conditions.MarkFalse(machineScope.AWSMachine, infrav1.SecurityGroupsReadyCondition, infrav1.SecurityGroupsFailedReason, clusterv1.ConditionSeverityError, err.Error()) + machineScope.Error(err, "unable to ensure security groups") + return err + } + conditions.MarkTrue(machineScope.AWSMachine, infrav1.SecurityGroupsReadyCondition) + + err = r.ensureInstanceMetadataOptions(ec2svc, instance, machineScope.AWSMachine) + if err != nil { + machineScope.Error(err, "failed to ensure instance metadata options") + return err + } + + return nil +} + func (r *AWSMachineReconciler) deleteEncryptedBootstrapDataSecret(machineScope *scope.MachineScope, clusterScope cloud.ClusterScoper) error { secretSvc, secretBackendErr := r.getSecretService(machineScope, clusterScope) if secretBackendErr != nil { @@ -1097,3 +1112,11 @@ func (r *AWSMachineReconciler) ensureStorageTags(ec2svc services.EC2Interface, i } } } + +func (r *AWSMachineReconciler) ensureInstanceMetadataOptions(ec2svc services.EC2Interface, instance *infrav1.Instance, machine *infrav1.AWSMachine) error { + if cmp.Equal(machine.Spec.InstanceMetadataOptions, instance.InstanceMetadataOptions) { + return nil + } + + return ec2svc.ModifyInstanceMetadataOptions(instance.ID, machine.Spec.InstanceMetadataOptions) +} diff --git a/docs/book/src/SUMMARY_PREFIX.md b/docs/book/src/SUMMARY_PREFIX.md index 03de396612..01a67be166 100644 --- a/docs/book/src/SUMMARY_PREFIX.md +++ b/docs/book/src/SUMMARY_PREFIX.md @@ -34,3 +34,4 @@ - [IAM Permissions Used](./topics/iam-permissions.md) - [Ignition support](./topics/ignition-support.md) - [External Resource Garbage Collection](./topics/external-resource-gc.md) + - [Instance Metadata](./topics/instance-metadata.md) diff --git a/docs/book/src/topics/instance-metadata.md b/docs/book/src/topics/instance-metadata.md new file mode 100644 index 0000000000..f87db0f8cf --- /dev/null +++ b/docs/book/src/topics/instance-metadata.md @@ -0,0 +1,31 @@ +# Instance Metadata Service + +Instance metadata is data about your instance that you can use to configure or manage the running instance which you can access from a running instance using one of the following methods: + +* Instance Metadata Service Version 1 (IMDSv1) – a request/response method +* Instance Metadata Service Version 2 (IMDSv2) – a session-oriented method + +CAPA defaults to IMDSv2 when creating instances, as it provides a [better level of security](https://aws.amazon.com/blogs/security/defense-in-depth-open-firewalls-reverse-proxies-ssrf-vulnerabilities-ec2-instance-metadata-service/). + +It is possible to configure the instance metadata options using the field called `instanceMetadataOptions` in the `AWSMachineTemplate`. + +Example: +```yaml +--- +apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 +kind: AWSMachineTemplate +metadata: + name: "test" +spec: + template: + spec: + instanceMetadataOptions: + httpEndpoint: enabled + httpPutResponseHopLimit: 1 + httpTokens: required + instanceMetadataTags: disabled +``` + +To use IMDSv1, simply set `httpTokens` value to `optional` (in other words, set the use of IMDSv2 to optional). + +See [the CLI command reference](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/ec2/modify-instance-metadata-options.html) for more information. diff --git a/exp/api/v1beta1/zz_generated.conversion.go b/exp/api/v1beta1/zz_generated.conversion.go index 2d05a61a1b..14a33e888b 100644 --- a/exp/api/v1beta1/zz_generated.conversion.go +++ b/exp/api/v1beta1/zz_generated.conversion.go @@ -310,11 +310,6 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } - if err := s.AddConversionFunc((*apiv1beta2.Instance)(nil), (*apiv1beta1.Instance)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_v1beta2_Instance_To_v1beta1_Instance(a.(*apiv1beta2.Instance), b.(*apiv1beta1.Instance), scope) - }); err != nil { - return err - } if err := s.AddConversionFunc((*v1beta2.RefreshPreferences)(nil), (*RefreshPreferences)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1beta2_RefreshPreferences_To_v1beta1_RefreshPreferences(a.(*v1beta2.RefreshPreferences), b.(*RefreshPreferences), scope) }); err != nil { diff --git a/pkg/cloud/services/ec2/instances.go b/pkg/cloud/services/ec2/instances.go index 5e1cfa4a29..550095dbb6 100644 --- a/pkg/cloud/services/ec2/instances.go +++ b/pkg/cloud/services/ec2/instances.go @@ -225,6 +225,8 @@ func (s *Service) CreateInstance(scope *scope.MachineScope, userData []byte, use input.SpotMarketOptions = scope.AWSMachine.Spec.SpotMarketOptions + input.InstanceMetadataOptions = scope.AWSMachine.Spec.InstanceMetadataOptions + input.Tenancy = scope.AWSMachine.Spec.Tenancy s.scope.Debug("Running instance", "machine-role", scope.Role()) @@ -571,6 +573,7 @@ func (s *Service) runInstance(role string, i *infrav1.Instance) (*infrav1.Instan } input.InstanceMarketOptions = getInstanceMarketOptionsRequest(i.SpotMarketOptions) + input.MetadataOptions = getInstanceMetadataOptionsRequest(i.InstanceMetadataOptions) if i.Tenancy != "" { input.Placement = &ec2.Placement{ @@ -816,6 +819,24 @@ func (s *Service) SDKToInstance(v *ec2.Instance) (*infrav1.Instance, error) { i.VolumeIDs = append(i.VolumeIDs, *volume.Ebs.VolumeId) } + if v.MetadataOptions != nil { + metadataOptions := &infrav1.InstanceMetadataOptions{} + if v.MetadataOptions.HttpEndpoint != nil { + metadataOptions.HTTPEndpoint = infrav1.InstanceMetadataState(*v.MetadataOptions.HttpEndpoint) + } + if v.MetadataOptions.HttpPutResponseHopLimit != nil { + metadataOptions.HTTPPutResponseHopLimit = *v.MetadataOptions.HttpPutResponseHopLimit + } + if v.MetadataOptions.HttpTokens != nil { + metadataOptions.HTTPTokens = infrav1.HTTPTokensState(*v.MetadataOptions.HttpTokens) + } + if v.MetadataOptions.InstanceMetadataTags != nil { + metadataOptions.InstanceMetadataTags = infrav1.InstanceMetadataState(*v.MetadataOptions.InstanceMetadataTags) + } + + i.InstanceMetadataOptions = metadataOptions + } + return i, nil } @@ -944,6 +965,23 @@ func (s *Service) checkRootVolume(rootVolume *infrav1.Volume, imageID string) (* return rootDeviceName, nil } +// ModifyInstanceMetadataOptions modifies the metadata options of the given EC2 instance. +func (s *Service) ModifyInstanceMetadataOptions(instanceID string, options *infrav1.InstanceMetadataOptions) error { + input := &ec2.ModifyInstanceMetadataOptionsInput{ + HttpEndpoint: aws.String(string(options.HTTPEndpoint)), + HttpPutResponseHopLimit: aws.Int64(options.HTTPPutResponseHopLimit), + HttpTokens: aws.String(string(options.HTTPTokens)), + InstanceMetadataTags: aws.String(string(options.InstanceMetadataTags)), + InstanceId: aws.String(instanceID), + } + + if _, err := s.EC2Client.ModifyInstanceMetadataOptions(input); err != nil { + return err + } + + return nil +} + // filterGroups filters a list for a string. func filterGroups(list []string, strToFilter string) (newList []string) { for _, item := range list { @@ -993,3 +1031,25 @@ func getInstanceMarketOptionsRequest(spotMarketOptions *infrav1.SpotMarketOption return instanceMarketOptionsRequest } + +func getInstanceMetadataOptionsRequest(metadataOptions *infrav1.InstanceMetadataOptions) *ec2.InstanceMetadataOptionsRequest { + if metadataOptions == nil { + return nil + } + + request := &ec2.InstanceMetadataOptionsRequest{} + if metadataOptions.HTTPEndpoint != "" { + request.SetHttpEndpoint(string(metadataOptions.HTTPEndpoint)) + } + if metadataOptions.HTTPPutResponseHopLimit != 0 { + request.SetHttpPutResponseHopLimit(metadataOptions.HTTPPutResponseHopLimit) + } + if metadataOptions.HTTPTokens != "" { + request.SetHttpTokens(string(metadataOptions.HTTPTokens)) + } + if metadataOptions.InstanceMetadataTags != "" { + request.SetInstanceMetadataTags(string(metadataOptions.InstanceMetadataTags)) + } + + return request +} diff --git a/pkg/cloud/services/interfaces.go b/pkg/cloud/services/interfaces.go index f0e03dde9a..992b2c5b8d 100644 --- a/pkg/cloud/services/interfaces.go +++ b/pkg/cloud/services/interfaces.go @@ -60,6 +60,7 @@ type EC2Interface interface { GetInstanceSecurityGroups(instanceID string) (map[string][]string, error) UpdateInstanceSecurityGroups(id string, securityGroups []string) error UpdateResourceTags(resourceID *string, create, remove map[string]string) error + ModifyInstanceMetadataOptions(instanceID string, options *infrav1.InstanceMetadataOptions) error TerminateInstanceAndWait(instanceID string) error DetachSecurityGroupsFromNetworkInterface(groups []string, interfaceID string) error diff --git a/pkg/cloud/services/mock_services/ec2_interface_mock.go b/pkg/cloud/services/mock_services/ec2_interface_mock.go index d3b754db1a..7f8cb07aaf 100644 --- a/pkg/cloud/services/mock_services/ec2_interface_mock.go +++ b/pkg/cloud/services/mock_services/ec2_interface_mock.go @@ -289,6 +289,20 @@ func (mr *MockEC2InterfaceMockRecorder) LaunchTemplateNeedsUpdate(arg0, arg1, ar return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LaunchTemplateNeedsUpdate", reflect.TypeOf((*MockEC2Interface)(nil).LaunchTemplateNeedsUpdate), arg0, arg1, arg2) } +// ModifyInstanceMetadataOptions mocks base method. +func (m *MockEC2Interface) ModifyInstanceMetadataOptions(arg0 string, arg1 *v1beta2.InstanceMetadataOptions) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ModifyInstanceMetadataOptions", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// ModifyInstanceMetadataOptions indicates an expected call of ModifyInstanceMetadataOptions. +func (mr *MockEC2InterfaceMockRecorder) ModifyInstanceMetadataOptions(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ModifyInstanceMetadataOptions", reflect.TypeOf((*MockEC2Interface)(nil).ModifyInstanceMetadataOptions), arg0, arg1) +} + // PruneLaunchTemplateVersions mocks base method. func (m *MockEC2Interface) PruneLaunchTemplateVersions(arg0 string) error { m.ctrl.T.Helper() diff --git a/test/e2e/suites/unmanaged/helpers_test.go b/test/e2e/suites/unmanaged/helpers_test.go index 5844f6e630..34373d6e8f 100644 --- a/test/e2e/suites/unmanaged/helpers_test.go +++ b/test/e2e/suites/unmanaged/helpers_test.go @@ -577,6 +577,29 @@ func assertSpotInstanceType(instanceID string) { Expect(len(result.Reservations[0].Instances)).To(Equal(1)) } +func assertInstanceMetadataOptions(instanceID string, expected infrav1.InstanceMetadataOptions) { + ginkgo.By(fmt.Sprintf("Finding EC2 instance with ID: %s", instanceID)) + ec2Client := ec2.New(e2eCtx.AWSSession) + input := &ec2.DescribeInstancesInput{ + InstanceIds: []*string{ + aws.String(instanceID[strings.LastIndex(instanceID, "/")+1:]), + }, + } + + result, err := ec2Client.DescribeInstances(input) + Expect(err).To(BeNil()) + Expect(len(result.Reservations)).To(Equal(1)) + Expect(len(result.Reservations[0].Instances)).To(Equal(1)) + + metadataOptions := result.Reservations[0].Instances[0].MetadataOptions + Expect(metadataOptions).ToNot(BeNil()) + + Expect(metadataOptions.HttpTokens).To(HaveValue(Equal(string(expected.HTTPTokens)))) // IMDSv2 enabled + Expect(metadataOptions.HttpEndpoint).To(HaveValue(Equal(string(expected.HTTPEndpoint)))) + Expect(metadataOptions.InstanceMetadataTags).To(HaveValue(Equal(string(expected.InstanceMetadataTags)))) + Expect(metadataOptions.HttpPutResponseHopLimit).To(HaveValue(Equal(expected.HTTPPutResponseHopLimit))) +} + func terminateInstance(instanceID string) { ginkgo.By(fmt.Sprintf("Terminating EC2 instance with ID: %s", instanceID)) ec2Client := ec2.New(e2eCtx.AWSSession) diff --git a/test/e2e/suites/unmanaged/unmanaged_functional_test.go b/test/e2e/suites/unmanaged/unmanaged_functional_test.go index a48ba2c7c6..71f714e0cc 100644 --- a/test/e2e/suites/unmanaged/unmanaged_functional_test.go +++ b/test/e2e/suites/unmanaged/unmanaged_functional_test.go @@ -193,6 +193,39 @@ var _ = ginkgo.Context("[unmanaged] [functional]", func() { Expect(err).To(BeNil()) Expect(awsCluster.Status.Bastion.State).To(Equal(infrav1.InstanceStateRunning)) expectAWSClusterConditions(awsCluster, []conditionAssertion{{infrav1.BastionHostReadyCondition, corev1.ConditionTrue, "", ""}}) + + mdName := clusterName + "-md01" + machineTempalte := makeAWSMachineTemplate(namespace.Name, mdName, e2eCtx.E2EConfig.GetVariable(shared.AwsNodeMachineType), nil) + machineTempalte.Spec.Template.Spec.InstanceMetadataOptions = &infrav1.InstanceMetadataOptions{ + HTTPEndpoint: infrav1.InstanceMetadataEndpointStateEnabled, + HTTPPutResponseHopLimit: 1, + HTTPTokens: infrav1.HTTPTokensStateRequired, // IMDSv2 + InstanceMetadataTags: infrav1.InstanceMetadataEndpointStateDisabled, + } + + machineDeployment := makeMachineDeployment(namespace.Name, mdName, clusterName, nil, int32(1)) + framework.CreateMachineDeployment(ctx, framework.CreateMachineDeploymentInput{ + Creator: e2eCtx.Environment.BootstrapClusterProxy.GetClient(), + MachineDeployment: machineDeployment, + BootstrapConfigTemplate: makeJoinBootstrapConfigTemplate(namespace.Name, mdName), + InfraMachineTemplate: machineTempalte, + }) + + framework.WaitForMachineDeploymentNodesToExist(ctx, framework.WaitForMachineDeploymentNodesToExistInput{ + Lister: e2eCtx.Environment.BootstrapClusterProxy.GetClient(), + Cluster: result.Cluster, + MachineDeployment: machineDeployment, + }, e2eCtx.E2EConfig.GetIntervals("", "wait-worker-nodes")...) + + workerMachines := framework.GetMachinesByMachineDeployments(ctx, framework.GetMachinesByMachineDeploymentsInput{ + Lister: e2eCtx.Environment.BootstrapClusterProxy.GetClient(), + ClusterName: clusterName, + Namespace: namespace.Name, + MachineDeployment: *machineDeployment, + }) + Expect(len(workerMachines)).To(Equal(1)) + + assertInstanceMetadataOptions(*workerMachines[0].Spec.ProviderID, *machineTempalte.Spec.Template.Spec.InstanceMetadataOptions) ginkgo.By("PASSED!") }) })