-
Notifications
You must be signed in to change notification settings - Fork 137
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Signed-off-by: Chris Doherty <[email protected]>
- Loading branch information
1 parent
5df464f
commit 6e39564
Showing
12 changed files
with
764 additions
and
19 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
package hardware | ||
|
||
import ( | ||
"context" | ||
"errors" | ||
"fmt" | ||
"net/http" | ||
|
||
"github.com/tinkerbell/tink/api/v1alpha2" | ||
"github.com/tinkerbell/tink/internal/hardware/internal" | ||
ctrl "sigs.k8s.io/controller-runtime" | ||
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" | ||
"sigs.k8s.io/controller-runtime/pkg/webhook" | ||
"sigs.k8s.io/controller-runtime/pkg/webhook/admission" | ||
) | ||
|
||
// admissionWebhookEndpoint is the endpoint serving the Admission handler. | ||
const admissionWebhookEndpoint = "/validate-tinkerbell-org-v1alpha2-hardware" | ||
|
||
// +kubebuilder:webhook:path=/validate-tinkerbell-org-v1alpha2-hardware,mutating=false,failurePolicy=fail,groups="",resources=hardware,verbs=create;update,versions=v1alpha2,name=hardware.tinkerbell.org | ||
|
||
// Admission handles complex validation for admitting a Hardware object to the cluster. | ||
type Admission struct { | ||
client ctrlclient.Client | ||
decoder *admission.Decoder | ||
} | ||
|
||
// Handle satisfies controller-runtime/pkg/webhook/admission#Handler. It is responsible for deciding | ||
// if the given req is valid and should be admitted to the cluster. | ||
func (a *Admission) Handle(ctx context.Context, req admission.Request) admission.Response { | ||
if a.client == nil { | ||
return admission.Errored(http.StatusInternalServerError, errors.New("misconfigured client")) | ||
} | ||
|
||
var hw v1alpha2.Hardware | ||
if err := a.decoder.Decode(req, &hw); err != nil { | ||
return admission.Errored(http.StatusBadRequest, err) | ||
} | ||
|
||
// Ensure conditionally optional fields are valid | ||
if resp := a.validateConditionalFields(&hw); !resp.Allowed { | ||
return resp | ||
} | ||
|
||
// Ensure MACs on the hardware are valid. | ||
if resp := a.validateMACs(&hw); !resp.Allowed { | ||
return resp | ||
} | ||
|
||
// Ensure there's no hardware in the cluster with the same MAC addresses. | ||
if resp := a.validateUniqueMACs(ctx, &hw); !resp.Allowed { | ||
return resp | ||
} | ||
|
||
// Ensure there's no hardware in the cluster with the same IP addresses. | ||
if resp := a.validateUniqueIPs(ctx, &hw); !resp.Allowed { | ||
return resp | ||
} | ||
|
||
return admission.Allowed("") | ||
} | ||
|
||
// InjectDecoder satisfies controller-runtime/pkg/webhook/admission#DecoderInjector. It is used | ||
// when registering the webhook to inject the decoder used by the controller manager. | ||
func (a *Admission) InjectDecoder(d *admission.Decoder) error { | ||
a.decoder = d | ||
return nil | ||
} | ||
|
||
// SetClient sets a's internal Kubernetes client. | ||
func (a *Admission) SetClient(c ctrlclient.Client) { | ||
a.client = c | ||
} | ||
|
||
// SetupWithManager registers a with mgr as a webhook served from AdmissionWebhookEndpoint. | ||
func (a *Admission) SetupWithManager(ctx context.Context, mgr ctrl.Manager) error { | ||
idx := mgr.GetFieldIndexer() | ||
|
||
err := idx.IndexField( | ||
ctx, | ||
&v1alpha2.Hardware{}, | ||
internal.HardwareByMACAddr, | ||
internal.HardwareByMACAddrFunc, | ||
) | ||
if err != nil { | ||
return fmt.Errorf("register index %s: %w", internal.HardwareByMACAddr, err) | ||
} | ||
|
||
err = idx.IndexField( | ||
ctx, | ||
&v1alpha2.Hardware{}, | ||
internal.HardwareByIPAddr, | ||
internal.HardwareByIPAddrFunc, | ||
) | ||
if err != nil { | ||
return fmt.Errorf("register index %s: %w", internal.HardwareByIPAddr, err) | ||
} | ||
|
||
mgr.GetWebhookServer().Register( | ||
admissionWebhookEndpoint, | ||
&webhook.Admission{Handler: a}, | ||
) | ||
|
||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
package hardware | ||
|
||
import ( | ||
"fmt" | ||
"net/http" | ||
|
||
"github.com/tinkerbell/tink/api/v1alpha2" | ||
"sigs.k8s.io/controller-runtime/pkg/webhook/admission" | ||
) | ||
|
||
func (a *Admission) validateConditionalFields(hw *v1alpha2.Hardware) admission.Response { | ||
for mac, ni := range hw.Spec.NetworkInterfaces { | ||
if ni.IsDHCPEnabled() && ni.DHCP == nil { | ||
return admission.Errored(http.StatusBadRequest, fmt.Errorf( | ||
"network interface for %v has DHCP enabled but no DHCP config", | ||
mac, | ||
)) | ||
} | ||
} | ||
|
||
return admission.Allowed("") | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
package hardware | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"net/http" | ||
"strings" | ||
|
||
"github.com/tinkerbell/tink/api/v1alpha2" | ||
"github.com/tinkerbell/tink/internal/hardware/internal" | ||
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" | ||
"sigs.k8s.io/controller-runtime/pkg/webhook/admission" | ||
) | ||
|
||
func (a *Admission) validateUniqueIPs(ctx context.Context, hw *v1alpha2.Hardware) admission.Response { | ||
// Determine if there are IP duplicates within the hw object. | ||
seen := map[string]struct{}{} | ||
var dupOnHw []string | ||
for _, ip := range hw.GetIPs() { | ||
if _, ok := seen[ip]; ok { | ||
dupOnHw = append(dupOnHw, ip) | ||
} | ||
seen[ip] = struct{}{} | ||
} | ||
|
||
if len(dupOnHw) > 0 { | ||
return admission.Errored(http.StatusBadRequest, fmt.Errorf( | ||
"duplicate IPs on Hardware: %v", | ||
strings.Join(dupOnHw, ", "), | ||
)) | ||
} | ||
|
||
// Determine if there are IP duplicates with other Hardware objects. | ||
dups := duplicates{} | ||
for _, ip := range hw.GetIPs() { | ||
var hwWithIP v1alpha2.HardwareList | ||
err := a.client.List(ctx, &hwWithIP, ctrlclient.MatchingFields{ | ||
internal.HardwareByIPAddr: ip, | ||
}) | ||
if err != nil { | ||
return admission.Errored(http.StatusInternalServerError, err) | ||
} | ||
if len(hwWithIP.Items) > 0 { | ||
dups.AppendTo(ip, hwWithIP.Items...) | ||
} | ||
} | ||
|
||
if len(dups) > 0 { | ||
return admission.Errored(http.StatusBadRequest, fmt.Errorf( | ||
"IP associated with existing Hardware: %v", | ||
dups.String(), | ||
)) | ||
} | ||
|
||
return admission.Allowed("") | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
package hardware | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"net/http" | ||
"regexp" | ||
"strings" | ||
|
||
"github.com/tinkerbell/tink/api/v1alpha2" | ||
"github.com/tinkerbell/tink/internal/hardware/internal" | ||
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" | ||
"sigs.k8s.io/controller-runtime/pkg/webhook/admission" | ||
) | ||
|
||
func (a *Admission) validateMACs(hw *v1alpha2.Hardware) admission.Response { | ||
// Validate all MACs on hw are valid before we compare them with Hardware in the cluster. | ||
if invalidMACs := getInvalidMACs(hw); len(invalidMACs) > 0 { | ||
return admission.Errored(http.StatusBadRequest, fmt.Errorf( | ||
"invalid MAC address (%v): %v", | ||
macRegex.String(), | ||
strings.Join(invalidMACs, ", "), | ||
)) | ||
} | ||
|
||
return admission.Allowed("") | ||
} | ||
|
||
// macRegex is taken from the API package documentation. It checks for valid MAC addresses. | ||
// It expects MACs to be lowercase which is necessary for index lookups on API objects. | ||
var macRegex = regexp.MustCompile("^([0-9a-f]{2}:){5}([0-9a-f]{2})$") | ||
|
||
func getInvalidMACs(hw *v1alpha2.Hardware) []string { | ||
var invalidMACs []string | ||
for _, mac := range hw.GetMACs() { | ||
if mac == "" { | ||
mac = "<empty string>" | ||
} | ||
if !macRegex.MatchString(mac) { | ||
invalidMACs = append(invalidMACs, mac) | ||
} | ||
} | ||
return invalidMACs | ||
} | ||
|
||
func (a *Admission) validateUniqueMACs(ctx context.Context, hw *v1alpha2.Hardware) admission.Response { | ||
dups := duplicates{} | ||
for _, mac := range hw.GetMACs() { | ||
var hwWithMAC v1alpha2.HardwareList | ||
err := a.client.List(ctx, &hwWithMAC, ctrlclient.MatchingFields{ | ||
internal.HardwareByMACAddr: mac, | ||
}) | ||
if err != nil { | ||
return admission.Errored(http.StatusInternalServerError, err) | ||
} | ||
|
||
if len(hwWithMAC.Items) > 0 { | ||
dups.AppendTo(mac, hwWithMAC.Items...) | ||
} | ||
} | ||
|
||
if len(dups) > 0 { | ||
return admission.Errored(http.StatusBadRequest, fmt.Errorf( | ||
"MAC associated with existing Hardware: %s", | ||
dups.String(), | ||
)) | ||
} | ||
|
||
return admission.Allowed("") | ||
} |
Oops, something went wrong.