Skip to content

Commit

Permalink
Merge pull request #4750 from vincepri/addproxytls-ignition
Browse files Browse the repository at this point in the history
🌱 Add support for Ignition v3 Proxy and TLS
  • Loading branch information
k8s-ci-robot authored Mar 6, 2024
2 parents 3a00c39 + 4073507 commit b655f99
Show file tree
Hide file tree
Showing 8 changed files with 496 additions and 3 deletions.
2 changes: 2 additions & 0 deletions api/v1beta1/zz_generated.conversion.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

61 changes: 61 additions & 0 deletions api/v1beta2/awsmachine_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
//
Expand Down Expand Up @@ -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.
Expand Down
126 changes: 124 additions & 2 deletions api/v1beta2/awsmachine_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
}

Expand Down
122 changes: 122 additions & 0 deletions api/v1beta2/awsmachine_webhook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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-",
Expand Down
Loading

0 comments on commit b655f99

Please sign in to comment.