Skip to content

Commit 4073507

Browse files
committed
Add support for Ignition v3 Proxy and TLS
From Ignition v3.1 there is support in the struct to setup a proxy, and CA validation. This changeset allows AWSMachines to expose these features when using Ignition. Signed-off-by: Vince Prignano <[email protected]>
1 parent 3a00c39 commit 4073507

8 files changed

+496
-3
lines changed

api/v1beta1/zz_generated.conversion.go

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/v1beta2/awsmachine_types.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,7 @@ type CloudInit struct {
210210
}
211211

212212
// Ignition defines options related to the bootstrapping systems where Ignition is used.
213+
// For more information on Ignition configuration, see https://coreos.github.io/butane/specs/
213214
type Ignition struct {
214215
// Version defines which version of Ignition will be used to generate bootstrap data.
215216
//
@@ -237,6 +238,66 @@ type Ignition struct {
237238
// +kubebuilder:default="ClusterObjectStore"
238239
// +kubebuilder:validation:Enum:="ClusterObjectStore";"UnencryptedUserData"
239240
StorageType IgnitionStorageTypeOption `json:"storageType,omitempty"`
241+
242+
// Proxy defines proxy settings for Ignition.
243+
// Only valid for Ignition versions 3.1 and above.
244+
// +optional
245+
Proxy *IgnitionProxy `json:"proxy,omitempty"`
246+
247+
// TLS defines TLS settings for Ignition.
248+
// Only valid for Ignition versions 3.1 and above.
249+
// +optional
250+
TLS *IgnitionTLS `json:"tls,omitempty"`
251+
}
252+
253+
// IgnitionCASource defines the source of the certificate authority to use for Ignition.
254+
// +kubebuilder:validation:MaxLength:=65536
255+
type IgnitionCASource string
256+
257+
// IgnitionTLS defines TLS settings for Ignition.
258+
type IgnitionTLS struct {
259+
// CASources defines the list of certificate authorities to use for Ignition.
260+
// The value is the certificate bundle (in PEM format). The bundle can contain multiple concatenated certificates.
261+
// Supported schemes are http, https, tftp, s3, arn, gs, and `data` (RFC 2397) URL scheme.
262+
//
263+
// +optional
264+
// +kubebuilder:validation:MaxItems=64
265+
CASources []IgnitionCASource `json:"certificateAuthorities,omitempty"`
266+
}
267+
268+
// IgnitionNoProxy defines the list of domains to not proxy for Ignition.
269+
// +kubebuilder:validation:MaxLength:=2048
270+
type IgnitionNoProxy string
271+
272+
// IgnitionProxy defines proxy settings for Ignition.
273+
type IgnitionProxy struct {
274+
// HTTPProxy is the HTTP proxy to use for Ignition.
275+
// A single URL that specifies the proxy server to use for HTTP and HTTPS requests,
276+
// unless overridden by the HTTPSProxy or NoProxy options.
277+
// +optional
278+
HTTPProxy *string `json:"httpProxy,omitempty"`
279+
280+
// HTTPSProxy is the HTTPS proxy to use for Ignition.
281+
// A single URL that specifies the proxy server to use for HTTPS requests,
282+
// unless overridden by the NoProxy option.
283+
// +optional
284+
HTTPSProxy *string `json:"httpsProxy,omitempty"`
285+
286+
// NoProxy is the list of domains to not proxy for Ignition.
287+
// Specifies a list of strings to hosts that should be excluded from proxying.
288+
//
289+
// Each value is represented by:
290+
// - An IP address prefix (1.2.3.4)
291+
// - An IP address prefix in CIDR notation (1.2.3.4/8)
292+
// - A domain name
293+
// - A domain name matches that name and all subdomains
294+
// - A domain name with a leading . matches subdomains only
295+
// - A special DNS label (*), indicates that no proxying should be done
296+
//
297+
// An IP address prefix and domain name can also include a literal port number (1.2.3.4:80).
298+
// +optional
299+
// +kubebuilder:validation:MaxItems=64
300+
NoProxy []IgnitionNoProxy `json:"noProxy,omitempty"`
240301
}
241302

242303
// AWSMachineStatus defines the observed state of AWSMachine.

api/v1beta2/awsmachine_webhook.go

Lines changed: 124 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,17 @@ limitations under the License.
1717
package v1beta2
1818

1919
import (
20+
"encoding/base64"
21+
"fmt"
22+
"net"
23+
"net/url"
24+
"strings"
25+
2026
"github.com/google/go-cmp/cmp"
2127
"github.com/pkg/errors"
2228
apierrors "k8s.io/apimachinery/pkg/api/errors"
2329
"k8s.io/apimachinery/pkg/runtime"
30+
"k8s.io/apimachinery/pkg/util/validation"
2431
"k8s.io/apimachinery/pkg/util/validation/field"
2532
ctrl "sigs.k8s.io/controller-runtime"
2633
"sigs.k8s.io/controller-runtime/pkg/webhook"
@@ -171,17 +178,132 @@ func (r *AWSMachine) ignitionEnabled() bool {
171178

172179
func (r *AWSMachine) validateIgnitionAndCloudInit() field.ErrorList {
173180
var allErrs field.ErrorList
181+
if !r.ignitionEnabled() {
182+
return allErrs
183+
}
174184

175185
// Feature gate is not enabled but ignition is enabled then send a forbidden error.
176-
if !feature.Gates.Enabled(feature.BootstrapFormatIgnition) && r.ignitionEnabled() {
186+
if !feature.Gates.Enabled(feature.BootstrapFormatIgnition) {
177187
allErrs = append(allErrs, field.Forbidden(field.NewPath("spec", "ignition"),
178188
"can be set only if the BootstrapFormatIgnition feature gate is enabled"))
179189
}
180190

181-
if r.ignitionEnabled() && r.cloudInitConfigured() {
191+
// If ignition is enabled, cloudInit should not be configured.
192+
if r.cloudInitConfigured() {
182193
allErrs = append(allErrs, field.Forbidden(field.NewPath("spec", "cloudInit"), "cannot be set if spec.ignition is set"))
183194
}
184195

196+
// Proxy and TLS are only valid for Ignition versions >= 3.1.
197+
if r.Spec.Ignition.Version == "2.3" || r.Spec.Ignition.Version == "3.0" {
198+
if r.Spec.Ignition.Proxy != nil {
199+
allErrs = append(allErrs, field.Forbidden(field.NewPath("spec", "ignition", "proxy"), "cannot be set if spec.ignition.version is 2.3 or 3.0"))
200+
}
201+
if r.Spec.Ignition.TLS != nil {
202+
allErrs = append(allErrs, field.Forbidden(field.NewPath("spec", "ignition", "tls"), "cannot be set if spec.ignition.version is 2.3 or 3.0"))
203+
}
204+
}
205+
206+
allErrs = append(allErrs, r.validateIgnitionProxy()...)
207+
allErrs = append(allErrs, r.validateIgnitionTLS()...)
208+
209+
return allErrs
210+
}
211+
212+
func (r *AWSMachine) validateIgnitionProxy() field.ErrorList {
213+
var allErrs field.ErrorList
214+
215+
if r.Spec.Ignition.Proxy == nil {
216+
return allErrs
217+
}
218+
219+
// Validate HTTPProxy.
220+
if r.Spec.Ignition.Proxy.HTTPProxy != nil {
221+
// Parse the url to check if it is valid.
222+
_, err := url.Parse(*r.Spec.Ignition.Proxy.HTTPProxy)
223+
if err != nil {
224+
allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "ignition", "proxy", "httpProxy"), *r.Spec.Ignition.Proxy.HTTPProxy, "invalid URL"))
225+
}
226+
}
227+
228+
// Validate HTTPSProxy.
229+
if r.Spec.Ignition.Proxy.HTTPSProxy != nil {
230+
// Parse the url to check if it is valid.
231+
_, err := url.Parse(*r.Spec.Ignition.Proxy.HTTPSProxy)
232+
if err != nil {
233+
allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "ignition", "proxy", "httpsProxy"), *r.Spec.Ignition.Proxy.HTTPSProxy, "invalid URL"))
234+
}
235+
}
236+
237+
// Validate NoProxy.
238+
for _, noProxy := range r.Spec.Ignition.Proxy.NoProxy {
239+
noProxy := string(noProxy)
240+
// Validate here that the value `noProxy` is:
241+
// - A domain name
242+
// - A domain name matches that name and all subdomains
243+
// - A domain name with a leading . matches subdomains only
244+
245+
// A special DNS label (*).
246+
if noProxy == "*" {
247+
continue
248+
}
249+
// An IP address prefix (1.2.3.4).
250+
if ip := net.ParseIP(noProxy); ip != nil {
251+
continue
252+
}
253+
// An IP address prefix in CIDR notation (1.2.3.4/8).
254+
if _, _, err := net.ParseCIDR(noProxy); err == nil {
255+
continue
256+
}
257+
// An IP or domain name with a port.
258+
if _, _, err := net.SplitHostPort(noProxy); err == nil {
259+
continue
260+
}
261+
// A domain name.
262+
if noProxy[0] == '.' {
263+
// If it starts with a dot, it should be a domain name.
264+
noProxy = noProxy[1:]
265+
}
266+
// Validate that the value matches DNS 1123.
267+
if errs := validation.IsDNS1123Subdomain(noProxy); len(errs) > 0 {
268+
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, "; "))))
269+
}
270+
}
271+
272+
return allErrs
273+
}
274+
275+
func (r *AWSMachine) validateIgnitionTLS() field.ErrorList {
276+
var allErrs field.ErrorList
277+
278+
if r.Spec.Ignition.TLS == nil {
279+
return allErrs
280+
}
281+
282+
for _, source := range r.Spec.Ignition.TLS.CASources {
283+
// Validate that source is RFC 2397 data URL.
284+
u, err := url.Parse(string(source))
285+
if err != nil {
286+
allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "ignition", "tls", "caSources"), source, "invalid URL"))
287+
}
288+
289+
switch u.Scheme {
290+
case "http", "https", "tftp", "s3", "arn", "gs":
291+
// Valid schemes.
292+
case "data":
293+
// Validate that the data URL is base64 encoded.
294+
i := strings.Index(u.Opaque, ",")
295+
if i < 0 {
296+
allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "ignition", "tls", "caSources"), source, "invalid data URL"))
297+
}
298+
// Validate that the data URL is base64 encoded.
299+
if _, err := base64.StdEncoding.DecodeString(u.Opaque[i+1:]); err != nil {
300+
allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "ignition", "tls", "caSources"), source, "invalid base64 encoding for data url"))
301+
}
302+
default:
303+
allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "ignition", "tls", "caSources"), source, "unsupported URL scheme"))
304+
}
305+
}
306+
185307
return allErrs
186308
}
187309

api/v1beta2/awsmachine_webhook_test.go

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,10 @@ import (
2424
"github.com/aws/aws-sdk-go/aws"
2525
. "github.com/onsi/gomega"
2626
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
27+
utilfeature "k8s.io/component-base/featuregate/testing"
2728
"k8s.io/utils/ptr"
2829

30+
"sigs.k8s.io/cluster-api-provider-aws/v2/feature"
2931
utildefaulting "sigs.k8s.io/cluster-api/util/defaulting"
3032
)
3133

@@ -248,9 +250,129 @@ func TestAWSMachineCreate(t *testing.T) {
248250
},
249251
wantErr: true,
250252
},
253+
{
254+
name: "ignition proxy and TLS can be from version 3.1",
255+
machine: &AWSMachine{
256+
Spec: AWSMachineSpec{
257+
InstanceType: "test",
258+
Ignition: &Ignition{
259+
Version: "3.1",
260+
Proxy: &IgnitionProxy{
261+
HTTPProxy: ptr.To("http://proxy.example.com:3128"),
262+
},
263+
TLS: &IgnitionTLS{
264+
CASources: []IgnitionCASource{"s3://example.com/ca.pem"},
265+
},
266+
},
267+
},
268+
},
269+
wantErr: false,
270+
},
271+
{
272+
name: "ignition tls with invalid CASources URL",
273+
machine: &AWSMachine{
274+
Spec: AWSMachineSpec{
275+
InstanceType: "test",
276+
Ignition: &Ignition{
277+
Version: "3.1",
278+
TLS: &IgnitionTLS{
279+
CASources: []IgnitionCASource{"data;;"},
280+
},
281+
},
282+
},
283+
},
284+
wantErr: true,
285+
},
286+
{
287+
name: "ignition proxy with valid URLs, and noproxy",
288+
machine: &AWSMachine{
289+
Spec: AWSMachineSpec{
290+
InstanceType: "test",
291+
Ignition: &Ignition{
292+
Version: "3.1",
293+
Proxy: &IgnitionProxy{
294+
HTTPProxy: ptr.To("http://proxy.example.com:3128"),
295+
HTTPSProxy: ptr.To("https://proxy.example.com:3128"),
296+
NoProxy: []IgnitionNoProxy{
297+
"10.0.0.1", // single ip
298+
"example.com", // domain
299+
".example.com", // all subdomains
300+
"example.com:3128", // domain with port
301+
"10.0.0.1:3128", // ip with port
302+
"10.0.0.0/8", // cidr block
303+
"*", // no proxy wildcard
304+
},
305+
},
306+
},
307+
},
308+
},
309+
wantErr: false,
310+
},
311+
{
312+
name: "ignition proxy with invalid HTTPProxy URL",
313+
machine: &AWSMachine{
314+
Spec: AWSMachineSpec{
315+
InstanceType: "test",
316+
Ignition: &Ignition{
317+
Version: "3.1",
318+
Proxy: &IgnitionProxy{
319+
HTTPProxy: ptr.To("*:80"),
320+
},
321+
},
322+
},
323+
},
324+
wantErr: true,
325+
},
326+
{
327+
name: "ignition proxy with invalid HTTPSProxy URL",
328+
machine: &AWSMachine{
329+
Spec: AWSMachineSpec{
330+
InstanceType: "test",
331+
Ignition: &Ignition{
332+
Version: "3.1",
333+
Proxy: &IgnitionProxy{
334+
HTTPSProxy: ptr.To("*:80"),
335+
},
336+
},
337+
},
338+
},
339+
wantErr: true,
340+
},
341+
{
342+
name: "ignition proxy with invalid noproxy URL",
343+
machine: &AWSMachine{
344+
Spec: AWSMachineSpec{
345+
InstanceType: "test",
346+
Ignition: &Ignition{
347+
Version: "3.1",
348+
Proxy: &IgnitionProxy{
349+
NoProxy: []IgnitionNoProxy{"&"},
350+
},
351+
},
352+
},
353+
},
354+
wantErr: true,
355+
},
356+
{
357+
name: "cannot use ignition proxy with version 2.3",
358+
machine: &AWSMachine{
359+
Spec: AWSMachineSpec{
360+
InstanceType: "test",
361+
Ignition: &Ignition{
362+
Version: "2.3.0",
363+
Proxy: &IgnitionProxy{
364+
HTTPProxy: ptr.To("http://proxy.example.com:3128"),
365+
},
366+
},
367+
},
368+
},
369+
wantErr: true,
370+
},
251371
}
252372
for _, tt := range tests {
253373
t.Run(tt.name, func(t *testing.T) {
374+
defer utilfeature.SetFeatureGateDuringTest(t, feature.Gates, feature.BootstrapFormatIgnition, true)()
375+
254376
machine := tt.machine.DeepCopy()
255377
machine.ObjectMeta = metav1.ObjectMeta{
256378
GenerateName: "machine-",

0 commit comments

Comments
 (0)