diff --git a/api/v1beta1/zz_generated.conversion.go b/api/v1beta1/zz_generated.conversion.go index 6fab23cc8a..30c7102779 100644 --- a/api/v1beta1/zz_generated.conversion.go +++ b/api/v1beta1/zz_generated.conversion.go @@ -1938,6 +1938,8 @@ func Convert_v1beta1_Ignition_To_v1beta2_Ignition(in *Ignition, out *v1beta2.Ign func autoConvert_v1beta2_Ignition_To_v1beta1_Ignition(in *v1beta2.Ignition, out *Ignition, s conversion.Scope) error { out.Version = in.Version // WARNING: in.StorageType requires manual conversion: does not exist in peer-type + // WARNING: in.Proxy requires manual conversion: does not exist in peer-type + // WARNING: in.TLS requires manual conversion: does not exist in peer-type return nil } diff --git a/api/v1beta2/awsmachine_types.go b/api/v1beta2/awsmachine_types.go index 10d8ce0dcb..c4fd5530ad 100644 --- a/api/v1beta2/awsmachine_types.go +++ b/api/v1beta2/awsmachine_types.go @@ -210,6 +210,7 @@ type CloudInit struct { } // Ignition defines options related to the bootstrapping systems where Ignition is used. +// For more information on Ignition configuration, see https://coreos.github.io/butane/specs/ type Ignition struct { // Version defines which version of Ignition will be used to generate bootstrap data. // @@ -237,6 +238,66 @@ type Ignition struct { // +kubebuilder:default="ClusterObjectStore" // +kubebuilder:validation:Enum:="ClusterObjectStore";"UnencryptedUserData" StorageType IgnitionStorageTypeOption `json:"storageType,omitempty"` + + // Proxy defines proxy settings for Ignition. + // Only valid for Ignition versions 3.1 and above. + // +optional + Proxy *IgnitionProxy `json:"proxy,omitempty"` + + // TLS defines TLS settings for Ignition. + // Only valid for Ignition versions 3.1 and above. + // +optional + TLS *IgnitionTLS `json:"tls,omitempty"` +} + +// IgnitionCASource defines the source of the certificate authority to use for Ignition. +// +kubebuilder:validation:MaxLength:=65536 +type IgnitionCASource string + +// IgnitionTLS defines TLS settings for Ignition. +type IgnitionTLS struct { + // CASources defines the list of certificate authorities to use for Ignition. + // The value is the certificate bundle (in PEM format). The bundle can contain multiple concatenated certificates. + // Supported schemes are http, https, tftp, s3, arn, gs, and `data` (RFC 2397) URL scheme. + // + // +optional + // +kubebuilder:validation:MaxItems=64 + CASources []IgnitionCASource `json:"certificateAuthorities,omitempty"` +} + +// IgnitionNoProxy defines the list of domains to not proxy for Ignition. +// +kubebuilder:validation:MaxLength:=2048 +type IgnitionNoProxy string + +// IgnitionProxy defines proxy settings for Ignition. +type IgnitionProxy struct { + // HTTPProxy is the HTTP proxy to use for Ignition. + // A single URL that specifies the proxy server to use for HTTP and HTTPS requests, + // unless overridden by the HTTPSProxy or NoProxy options. + // +optional + HTTPProxy *string `json:"httpProxy,omitempty"` + + // HTTPSProxy is the HTTPS proxy to use for Ignition. + // A single URL that specifies the proxy server to use for HTTPS requests, + // unless overridden by the NoProxy option. + // +optional + HTTPSProxy *string `json:"httpsProxy,omitempty"` + + // NoProxy is the list of domains to not proxy for Ignition. + // Specifies a list of strings to hosts that should be excluded from proxying. + // + // Each value is represented by: + // - An IP address prefix (1.2.3.4) + // - An IP address prefix in CIDR notation (1.2.3.4/8) + // - A domain name + // - A domain name matches that name and all subdomains + // - A domain name with a leading . matches subdomains only + // - A special DNS label (*), indicates that no proxying should be done + // + // An IP address prefix and domain name can also include a literal port number (1.2.3.4:80). + // +optional + // +kubebuilder:validation:MaxItems=64 + NoProxy []IgnitionNoProxy `json:"noProxy,omitempty"` } // AWSMachineStatus defines the observed state of AWSMachine. diff --git a/api/v1beta2/awsmachine_webhook.go b/api/v1beta2/awsmachine_webhook.go index 2fe32083db..8938e01dfb 100644 --- a/api/v1beta2/awsmachine_webhook.go +++ b/api/v1beta2/awsmachine_webhook.go @@ -17,10 +17,17 @@ limitations under the License. package v1beta2 import ( + "encoding/base64" + "fmt" + "net" + "net/url" + "strings" + "github.com/google/go-cmp/cmp" "github.com/pkg/errors" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/validation" "k8s.io/apimachinery/pkg/util/validation/field" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/webhook" @@ -171,17 +178,132 @@ func (r *AWSMachine) ignitionEnabled() bool { func (r *AWSMachine) validateIgnitionAndCloudInit() field.ErrorList { var allErrs field.ErrorList + if !r.ignitionEnabled() { + return allErrs + } // Feature gate is not enabled but ignition is enabled then send a forbidden error. - if !feature.Gates.Enabled(feature.BootstrapFormatIgnition) && r.ignitionEnabled() { + if !feature.Gates.Enabled(feature.BootstrapFormatIgnition) { allErrs = append(allErrs, field.Forbidden(field.NewPath("spec", "ignition"), "can be set only if the BootstrapFormatIgnition feature gate is enabled")) } - if r.ignitionEnabled() && r.cloudInitConfigured() { + // If ignition is enabled, cloudInit should not be configured. + if r.cloudInitConfigured() { allErrs = append(allErrs, field.Forbidden(field.NewPath("spec", "cloudInit"), "cannot be set if spec.ignition is set")) } + // Proxy and TLS are only valid for Ignition versions >= 3.1. + if r.Spec.Ignition.Version == "2.3" || r.Spec.Ignition.Version == "3.0" { + if r.Spec.Ignition.Proxy != nil { + allErrs = append(allErrs, field.Forbidden(field.NewPath("spec", "ignition", "proxy"), "cannot be set if spec.ignition.version is 2.3 or 3.0")) + } + if r.Spec.Ignition.TLS != nil { + allErrs = append(allErrs, field.Forbidden(field.NewPath("spec", "ignition", "tls"), "cannot be set if spec.ignition.version is 2.3 or 3.0")) + } + } + + allErrs = append(allErrs, r.validateIgnitionProxy()...) + allErrs = append(allErrs, r.validateIgnitionTLS()...) + + return allErrs +} + +func (r *AWSMachine) validateIgnitionProxy() field.ErrorList { + var allErrs field.ErrorList + + if r.Spec.Ignition.Proxy == nil { + return allErrs + } + + // Validate HTTPProxy. + if r.Spec.Ignition.Proxy.HTTPProxy != nil { + // Parse the url to check if it is valid. + _, err := url.Parse(*r.Spec.Ignition.Proxy.HTTPProxy) + if err != nil { + allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "ignition", "proxy", "httpProxy"), *r.Spec.Ignition.Proxy.HTTPProxy, "invalid URL")) + } + } + + // Validate HTTPSProxy. + if r.Spec.Ignition.Proxy.HTTPSProxy != nil { + // Parse the url to check if it is valid. + _, err := url.Parse(*r.Spec.Ignition.Proxy.HTTPSProxy) + if err != nil { + allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "ignition", "proxy", "httpsProxy"), *r.Spec.Ignition.Proxy.HTTPSProxy, "invalid URL")) + } + } + + // Validate NoProxy. + for _, noProxy := range r.Spec.Ignition.Proxy.NoProxy { + noProxy := string(noProxy) + // Validate here that the value `noProxy` is: + // - A domain name + // - A domain name matches that name and all subdomains + // - A domain name with a leading . matches subdomains only + + // A special DNS label (*). + if noProxy == "*" { + continue + } + // An IP address prefix (1.2.3.4). + if ip := net.ParseIP(noProxy); ip != nil { + continue + } + // An IP address prefix in CIDR notation (1.2.3.4/8). + if _, _, err := net.ParseCIDR(noProxy); err == nil { + continue + } + // An IP or domain name with a port. + if _, _, err := net.SplitHostPort(noProxy); err == nil { + continue + } + // A domain name. + if noProxy[0] == '.' { + // If it starts with a dot, it should be a domain name. + noProxy = noProxy[1:] + } + // Validate that the value matches DNS 1123. + if errs := validation.IsDNS1123Subdomain(noProxy); len(errs) > 0 { + allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "ignition", "proxy", "noProxy"), noProxy, fmt.Sprintf("invalid noProxy value, please refer to the field documentation: %s", strings.Join(errs, "; ")))) + } + } + + return allErrs +} + +func (r *AWSMachine) validateIgnitionTLS() field.ErrorList { + var allErrs field.ErrorList + + if r.Spec.Ignition.TLS == nil { + return allErrs + } + + for _, source := range r.Spec.Ignition.TLS.CASources { + // Validate that source is RFC 2397 data URL. + u, err := url.Parse(string(source)) + if err != nil { + allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "ignition", "tls", "caSources"), source, "invalid URL")) + } + + switch u.Scheme { + case "http", "https", "tftp", "s3", "arn", "gs": + // Valid schemes. + case "data": + // Validate that the data URL is base64 encoded. + i := strings.Index(u.Opaque, ",") + if i < 0 { + allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "ignition", "tls", "caSources"), source, "invalid data URL")) + } + // Validate that the data URL is base64 encoded. + if _, err := base64.StdEncoding.DecodeString(u.Opaque[i+1:]); err != nil { + allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "ignition", "tls", "caSources"), source, "invalid base64 encoding for data url")) + } + default: + allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "ignition", "tls", "caSources"), source, "unsupported URL scheme")) + } + } + return allErrs } diff --git a/api/v1beta2/awsmachine_webhook_test.go b/api/v1beta2/awsmachine_webhook_test.go index a2b6ecd607..8588211aa7 100644 --- a/api/v1beta2/awsmachine_webhook_test.go +++ b/api/v1beta2/awsmachine_webhook_test.go @@ -24,8 +24,10 @@ import ( "github.com/aws/aws-sdk-go/aws" . "github.com/onsi/gomega" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + utilfeature "k8s.io/component-base/featuregate/testing" "k8s.io/utils/ptr" + "sigs.k8s.io/cluster-api-provider-aws/v2/feature" utildefaulting "sigs.k8s.io/cluster-api/util/defaulting" ) @@ -248,9 +250,129 @@ func TestAWSMachineCreate(t *testing.T) { }, wantErr: true, }, + { + name: "ignition proxy and TLS can be from version 3.1", + machine: &AWSMachine{ + Spec: AWSMachineSpec{ + InstanceType: "test", + Ignition: &Ignition{ + Version: "3.1", + Proxy: &IgnitionProxy{ + HTTPProxy: ptr.To("http://proxy.example.com:3128"), + }, + TLS: &IgnitionTLS{ + CASources: []IgnitionCASource{"s3://example.com/ca.pem"}, + }, + }, + }, + }, + wantErr: false, + }, + { + name: "ignition tls with invalid CASources URL", + machine: &AWSMachine{ + Spec: AWSMachineSpec{ + InstanceType: "test", + Ignition: &Ignition{ + Version: "3.1", + TLS: &IgnitionTLS{ + CASources: []IgnitionCASource{"data;;"}, + }, + }, + }, + }, + wantErr: true, + }, + { + name: "ignition proxy with valid URLs, and noproxy", + machine: &AWSMachine{ + Spec: AWSMachineSpec{ + InstanceType: "test", + Ignition: &Ignition{ + Version: "3.1", + Proxy: &IgnitionProxy{ + HTTPProxy: ptr.To("http://proxy.example.com:3128"), + HTTPSProxy: ptr.To("https://proxy.example.com:3128"), + NoProxy: []IgnitionNoProxy{ + "10.0.0.1", // single ip + "example.com", // domain + ".example.com", // all subdomains + "example.com:3128", // domain with port + "10.0.0.1:3128", // ip with port + "10.0.0.0/8", // cidr block + "*", // no proxy wildcard + }, + }, + }, + }, + }, + wantErr: false, + }, + { + name: "ignition proxy with invalid HTTPProxy URL", + machine: &AWSMachine{ + Spec: AWSMachineSpec{ + InstanceType: "test", + Ignition: &Ignition{ + Version: "3.1", + Proxy: &IgnitionProxy{ + HTTPProxy: ptr.To("*:80"), + }, + }, + }, + }, + wantErr: true, + }, + { + name: "ignition proxy with invalid HTTPSProxy URL", + machine: &AWSMachine{ + Spec: AWSMachineSpec{ + InstanceType: "test", + Ignition: &Ignition{ + Version: "3.1", + Proxy: &IgnitionProxy{ + HTTPSProxy: ptr.To("*:80"), + }, + }, + }, + }, + wantErr: true, + }, + { + name: "ignition proxy with invalid noproxy URL", + machine: &AWSMachine{ + Spec: AWSMachineSpec{ + InstanceType: "test", + Ignition: &Ignition{ + Version: "3.1", + Proxy: &IgnitionProxy{ + NoProxy: []IgnitionNoProxy{"&"}, + }, + }, + }, + }, + wantErr: true, + }, + { + name: "cannot use ignition proxy with version 2.3", + machine: &AWSMachine{ + Spec: AWSMachineSpec{ + InstanceType: "test", + Ignition: &Ignition{ + Version: "2.3.0", + Proxy: &IgnitionProxy{ + HTTPProxy: ptr.To("http://proxy.example.com:3128"), + }, + }, + }, + }, + wantErr: true, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + defer utilfeature.SetFeatureGateDuringTest(t, feature.Gates, feature.BootstrapFormatIgnition, true)() + machine := tt.machine.DeepCopy() machine.ObjectMeta = metav1.ObjectMeta{ GenerateName: "machine-", diff --git a/api/v1beta2/zz_generated.deepcopy.go b/api/v1beta2/zz_generated.deepcopy.go index fa6fe0e594..ee0dc510c2 100644 --- a/api/v1beta2/zz_generated.deepcopy.go +++ b/api/v1beta2/zz_generated.deepcopy.go @@ -743,7 +743,7 @@ func (in *AWSMachineSpec) DeepCopyInto(out *AWSMachineSpec) { if in.Ignition != nil { in, out := &in.Ignition, &out.Ignition *out = new(Ignition) - **out = **in + (*in).DeepCopyInto(*out) } if in.SpotMarketOptions != nil { in, out := &in.SpotMarketOptions, &out.SpotMarketOptions @@ -1332,6 +1332,16 @@ func (in *IPv6) DeepCopy() *IPv6 { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Ignition) DeepCopyInto(out *Ignition) { *out = *in + if in.Proxy != nil { + in, out := &in.Proxy, &out.Proxy + *out = new(IgnitionProxy) + (*in).DeepCopyInto(*out) + } + if in.TLS != nil { + in, out := &in.TLS, &out.TLS + *out = new(IgnitionTLS) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Ignition. @@ -1344,6 +1354,56 @@ func (in *Ignition) DeepCopy() *Ignition { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *IgnitionProxy) DeepCopyInto(out *IgnitionProxy) { + *out = *in + if in.HTTPProxy != nil { + in, out := &in.HTTPProxy, &out.HTTPProxy + *out = new(string) + **out = **in + } + if in.HTTPSProxy != nil { + in, out := &in.HTTPSProxy, &out.HTTPSProxy + *out = new(string) + **out = **in + } + if in.NoProxy != nil { + in, out := &in.NoProxy, &out.NoProxy + *out = make([]IgnitionNoProxy, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IgnitionProxy. +func (in *IgnitionProxy) DeepCopy() *IgnitionProxy { + if in == nil { + return nil + } + out := new(IgnitionProxy) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *IgnitionTLS) DeepCopyInto(out *IgnitionTLS) { + *out = *in + if in.CASources != nil { + in, out := &in.CASources, &out.CASources + *out = make([]IgnitionCASource, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IgnitionTLS. +func (in *IgnitionTLS) DeepCopy() *IgnitionTLS { + if in == nil { + return nil + } + out := new(IgnitionTLS) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *IngressRule) DeepCopyInto(out *IngressRule) { *out = *in 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 e356896c1b..2ad97a52d3 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachines.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachines.yaml @@ -632,6 +632,40 @@ spec: description: Ignition defined options related to the bootstrapping systems where Ignition is used. properties: + proxy: + description: Proxy defines proxy settings for Ignition. Only valid + for Ignition versions 3.1 and above. + properties: + httpProxy: + description: HTTPProxy is the HTTP proxy to use for Ignition. + A single URL that specifies the proxy server to use for + HTTP and HTTPS requests, unless overridden by the HTTPSProxy + or NoProxy options. + type: string + httpsProxy: + description: HTTPSProxy is the HTTPS proxy to use for Ignition. + A single URL that specifies the proxy server to use for + HTTPS requests, unless overridden by the NoProxy option. + type: string + noProxy: + description: "NoProxy is the list of domains to not proxy + for Ignition. Specifies a list of strings to hosts that + should be excluded from proxying. \n Each value is represented + by: - An IP address prefix (1.2.3.4) - An IP address prefix + in CIDR notation (1.2.3.4/8) - A domain name - A domain + name matches that name and all subdomains - A domain name + with a leading . matches subdomains only - A special DNS + label (*), indicates that no proxying should be done \n + An IP address prefix and domain name can also include a + literal port number (1.2.3.4:80)." + items: + description: IgnitionNoProxy defines the list of domains + to not proxy for Ignition. + maxLength: 2048 + type: string + maxItems: 64 + type: array + type: object storageType: default: ClusterObjectStore description: "StorageType defines how to store the boostrap user @@ -654,6 +688,24 @@ spec: - ClusterObjectStore - UnencryptedUserData type: string + tls: + description: TLS defines TLS settings for Ignition. Only valid + for Ignition versions 3.1 and above. + properties: + certificateAuthorities: + description: CASources defines the list of certificate authorities + to use for Ignition. The value is the certificate bundle + (in PEM format). The bundle can contain multiple concatenated + certificates. Supported schemes are http, https, tftp, s3, + arn, gs, and `data` (RFC 2397) URL scheme. + items: + description: IgnitionCASource defines the source of the + certificate authority to use for Ignition. + maxLength: 65536 + type: string + maxItems: 64 + type: array + type: object version: default: "2.3" description: Version defines which version of Ignition will be 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 00b85b4969..dec0817523 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachinetemplates.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachinetemplates.yaml @@ -578,6 +578,42 @@ spec: description: Ignition defined options related to the bootstrapping systems where Ignition is used. properties: + proxy: + description: Proxy defines proxy settings for Ignition. + Only valid for Ignition versions 3.1 and above. + properties: + httpProxy: + description: HTTPProxy is the HTTP proxy to use for + Ignition. A single URL that specifies the proxy + server to use for HTTP and HTTPS requests, unless + overridden by the HTTPSProxy or NoProxy options. + type: string + httpsProxy: + description: HTTPSProxy is the HTTPS proxy to use + for Ignition. A single URL that specifies the proxy + server to use for HTTPS requests, unless overridden + by the NoProxy option. + type: string + noProxy: + description: "NoProxy is the list of domains to not + proxy for Ignition. Specifies a list of strings + to hosts that should be excluded from proxying. + \n Each value is represented by: - An IP address + prefix (1.2.3.4) - An IP address prefix in CIDR + notation (1.2.3.4/8) - A domain name - A domain + name matches that name and all subdomains - A domain + name with a leading . matches subdomains only - + A special DNS label (*), indicates that no proxying + should be done \n An IP address prefix and domain + name can also include a literal port number (1.2.3.4:80)." + items: + description: IgnitionNoProxy defines the list of + domains to not proxy for Ignition. + maxLength: 2048 + type: string + maxItems: 64 + type: array + type: object storageType: default: ClusterObjectStore description: "StorageType defines how to store the boostrap @@ -602,6 +638,25 @@ spec: - ClusterObjectStore - UnencryptedUserData type: string + tls: + description: TLS defines TLS settings for Ignition. Only + valid for Ignition versions 3.1 and above. + properties: + certificateAuthorities: + description: CASources defines the list of certificate + authorities to use for Ignition. The value is the + certificate bundle (in PEM format). The bundle can + contain multiple concatenated certificates. Supported + schemes are http, https, tftp, s3, arn, gs, and + `data` (RFC 2397) URL scheme. + items: + description: IgnitionCASource defines the source + of the certificate authority to use for Ignition. + maxLength: 65536 + type: string + maxItems: 64 + type: array + type: object version: default: "2.3" description: Version defines which version of Ignition diff --git a/controllers/awsmachine_controller.go b/controllers/awsmachine_controller.go index 038e849edb..14bb9387a1 100644 --- a/controllers/awsmachine_controller.go +++ b/controllers/awsmachine_controller.go @@ -807,6 +807,25 @@ func (r *AWSMachineReconciler) generateIgnitionWithRemoteStorage(scope *scope.Ma }, } + if scope.AWSMachine.Spec.Ignition.Proxy != nil { + ignData.Ignition.Proxy = ignV3Types.Proxy{ + HTTPProxy: scope.AWSMachine.Spec.Ignition.Proxy.HTTPProxy, + HTTPSProxy: scope.AWSMachine.Spec.Ignition.Proxy.HTTPSProxy, + } + for _, noProxy := range scope.AWSMachine.Spec.Ignition.Proxy.NoProxy { + ignData.Ignition.Proxy.NoProxy = append(ignData.Ignition.Proxy.NoProxy, ignV3Types.NoProxyItem(noProxy)) + } + } + + if scope.AWSMachine.Spec.Ignition.TLS != nil { + for _, cert := range scope.AWSMachine.Spec.Ignition.TLS.CASources { + ignData.Ignition.Security.TLS.CertificateAuthorities = append( + ignData.Ignition.Security.TLS.CertificateAuthorities, + ignV3Types.Resource{Source: aws.String(string(cert))}, + ) + } + } + return json.Marshal(ignData) default: return nil, errors.Errorf("unsupported ignition version %q", ignVersion)