Skip to content

Commit

Permalink
[api] add initial External API with basic validation
Browse files Browse the repository at this point in the history
  • Loading branch information
Frostman committed Dec 8, 2023
1 parent c827b60 commit 1345ecb
Show file tree
Hide file tree
Showing 22 changed files with 908 additions and 28 deletions.
57 changes: 51 additions & 6 deletions api/vpc/v1alpha2/external_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,22 +19,34 @@ package v1alpha2
import (
"context"

"github.com/pkg/errors"
wiringapi "go.githedgehog.com/fabric/api/wiring/v1alpha2"
"go.githedgehog.com/fabric/pkg/manager/validation"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
)

// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized.

// ExternalSpec defines the desired state of External
type ExternalSpec struct{}
type ExternalSpec struct {
IPv4Namespace string `json:"ipv4Namespace,omitempty"`
InboundCommunity string `json:"inboundCommunity,omitempty"`
OutboundCommunity string `json:"outboundCommunity,omitempty"`
}

// ExternalStatus defines the observed state of External
type ExternalStatus struct{}

//+kubebuilder:object:root=true
//+kubebuilder:subresource:status

// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:resource:categories=hedgehog;fabric;external,shortName=ext
// +kubebuilder:printcolumn:name="IPv4NS",type=string,JSONPath=`.spec.ipv4Namespace`,priority=0
// +kubebuilder:printcolumn:name="InComm",type=string,JSONPath=`.spec.inboundCommunity`,priority=0
// +kubebuilder:printcolumn:name="OutComm",type=string,JSONPath=`.spec.outboundCommunity`,priority=0
// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`,priority=0
// External is the Schema for the externals API
type External struct {
metav1.TypeMeta `json:",inline"`
Expand All @@ -58,9 +70,42 @@ func init() {
}

func (external *External) Default() {
// TODO
if external.Spec.IPv4Namespace == "" {
external.Spec.IPv4Namespace = "default"
}

if external.Labels == nil {
external.Labels = map[string]string{}
}

wiringapi.CleanupFabricLabels(external.Labels)

external.Labels[LabelIPv4NS] = external.Spec.IPv4Namespace
}

func (external *External) Validate(ctx context.Context, client validation.Client) (admission.Warnings, error) {
return nil, nil // TODO
if external.Spec.IPv4Namespace == "" {
return nil, errors.Errorf("IPv4Namespace is required")
}
if external.Spec.InboundCommunity == "" {
return nil, errors.Errorf("inboundCommunity is required")
}
if external.Spec.OutboundCommunity == "" {
return nil, errors.Errorf("outboundCommunity is required")
}

// TODO validate communities

if client != nil {
ipNs := &IPv4Namespace{}
err := client.Get(ctx, types.NamespacedName{Name: external.Spec.IPv4Namespace, Namespace: external.Namespace}, ipNs)
if err != nil {
if apierrors.IsNotFound(err) {
return nil, errors.Errorf("IPv4Namespace %s not found", external.Spec.IPv4Namespace)
}
return nil, errors.Wrapf(err, "failed to get IPv4Namespace %s", external.Spec.IPv4Namespace) // TODO replace with some internal error to not expose to the user
}
}

return nil, nil
}
90 changes: 84 additions & 6 deletions api/vpc/v1alpha2/externalattachment_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,22 +19,48 @@ package v1alpha2
import (
"context"

"github.com/pkg/errors"
wiringapi "go.githedgehog.com/fabric/api/wiring/v1alpha2"
"go.githedgehog.com/fabric/pkg/manager/validation"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
)

// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized.

// ExternalAttachmentSpec defines the desired state of ExternalAttachment
type ExternalAttachmentSpec struct{}
type ExternalAttachmentSpec struct {
External string `json:"external,omitempty"`
Connection string `json:"connection,omitempty"`
Switch ExternalAttachmentSwitch `json:"switch,omitempty"`
Neighbor ExternalAttachmentNeighbor `json:"neighbor,omitempty"`
}

type ExternalAttachmentSwitch struct {
VLAN uint16 `json:"vlan,omitempty"`
IP string `json:"ip,omitempty"`
}

type ExternalAttachmentNeighbor struct {
ASN uint32 `json:"asn,omitempty"`
IP string `json:"ip,omitempty"`
}

// ExternalAttachmentStatus defines the observed state of ExternalAttachment
type ExternalAttachmentStatus struct{}

//+kubebuilder:object:root=true
//+kubebuilder:subresource:status

// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:resource:categories=hedgehog;fabric;external,shortName=extattach
// +kubebuilder:printcolumn:name="External",type=string,JSONPath=`.spec.external`,priority=0
// +kubebuilder:printcolumn:name="Connection",type=string,JSONPath=`.spec.connection`,priority=0
// +kubebuilder:printcolumn:name="SwVLAN",type=string,JSONPath=`.spec.switch.vlan`,priority=1
// +kubebuilder:printcolumn:name="SwIP",type=string,JSONPath=`.spec.switch.ip`,priority=1
// +kubebuilder:printcolumn:name="NeighASN",type=string,JSONPath=`.spec.neighbor.asn`,priority=1
// +kubebuilder:printcolumn:name="NeighIP",type=string,JSONPath=`.spec.neighbor.ip`,priority=1
// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`,priority=0
// ExternalAttachment is the Schema for the externalattachments API
type ExternalAttachment struct {
metav1.TypeMeta `json:",inline"`
Expand All @@ -58,9 +84,61 @@ func init() {
}

func (attach *ExternalAttachment) Default() {
// TODO
if attach.Labels == nil {
attach.Labels = map[string]string{}
}

wiringapi.CleanupFabricLabels(attach.Labels)

attach.Labels[wiringapi.LabelConnection] = attach.Spec.Connection
attach.Labels[LabelExternal] = attach.Spec.External
}

func (attach *ExternalAttachment) Validate(ctx context.Context, client validation.Client) (admission.Warnings, error) {
return nil, nil // TODO
if attach.Spec.External == "" {
return nil, errors.Errorf("external is required")
}
if attach.Spec.Connection == "" {
return nil, errors.Errorf("connection is required")
}
if attach.Spec.Switch.VLAN == 0 {
return nil, errors.Errorf("switch.vlan is required")
}
if attach.Spec.Switch.IP == "" {
return nil, errors.Errorf("switch.ip is required")
}
if attach.Spec.Neighbor.ASN == 0 {
return nil, errors.Errorf("neighbor.asn is required")
}
if attach.Spec.Neighbor.IP == "" {
return nil, errors.Errorf("neighbor.ip is required")
}

if client != nil {
ext := &External{}
if err := client.Get(ctx, types.NamespacedName{Name: attach.Spec.External, Namespace: attach.Namespace}, ext); err != nil {
if apierrors.IsNotFound(err) {
return nil, errors.Errorf("external %s not found", attach.Spec.External)
}

return nil, errors.Wrapf(err, "failed to read external %s", attach.Spec.External) // TODO replace with some internal error to not expose to the user
}

conn := &wiringapi.Connection{}
if err := client.Get(ctx, types.NamespacedName{Name: attach.Spec.Connection, Namespace: attach.Namespace}, conn); err != nil {
if apierrors.IsNotFound(err) {
return nil, errors.Errorf("connection %s not found", attach.Spec.Connection)
}

return nil, errors.Wrapf(err, "failed to read connection %s", attach.Spec.Connection) // TODO replace with some internal error to not expose to the user
}

if conn.Spec.External == nil {
return nil, errors.Errorf("connection %s is not external", attach.Spec.Connection)
}

// TODO validate IPs/ASNs/VLANs
}

return nil, nil
}
107 changes: 101 additions & 6 deletions api/vpc/v1alpha2/externalpeering_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,23 +18,56 @@ package v1alpha2

import (
"context"
"sort"

"github.com/pkg/errors"
wiringapi "go.githedgehog.com/fabric/api/wiring/v1alpha2"
"go.githedgehog.com/fabric/pkg/manager/validation"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
)

// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized.

// ExternalPeeringSpec defines the desired state of ExternalPeering
type ExternalPeeringSpec struct{}
type ExternalPeeringSpec struct {
Permit ExternalPeeringSpecPermit `json:"permit,omitempty"`
}

type ExternalPeeringSpecPermit struct {
VPC ExternalPeeringSpecVPC `json:"vpc,omitempty"`
External ExternalPeeringSpecExternal `json:"external,omitempty"`
}

type ExternalPeeringSpecVPC struct {
Name string `json:"name,omitempty"`
Subnets []string `json:"subnets,omitempty"`
}

type ExternalPeeringSpecExternal struct {
Name string `json:"name,omitempty"`
Prefixes []ExternalPeeringSpecPrefix `json:"prefixes,omitempty"`
}

type ExternalPeeringSpecPrefix struct {
Prefix string `json:"prefix,omitempty"`
Ge uint8 `json:"ge,omitempty"`
Le uint8 `json:"le,omitempty"`
}

// ExternalPeeringStatus defines the observed state of ExternalPeering
type ExternalPeeringStatus struct{}

//+kubebuilder:object:root=true
//+kubebuilder:subresource:status

// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:resource:categories=hedgehog;fabric;external,shortName=extpeering;extpeer
// +kubebuilder:printcolumn:name="VPC",type=string,JSONPath=`.spec.permit.vpc.name`,priority=0
// +kubebuilder:printcolumn:name="VPCSubnets",type=string,JSONPath=`.spec.permit.vpc.subnets`,priority=1
// +kubebuilder:printcolumn:name="External",type=string,JSONPath=`.spec.permit.external.name`,priority=0
// +kubebuilder:printcolumn:name="ExtPrefixes",type=string,JSONPath=`.spec.permit.external.prefixes`,priority=1
// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`,priority=0
// ExternalPeering is the Schema for the externalpeerings API
type ExternalPeering struct {
metav1.TypeMeta `json:",inline"`
Expand All @@ -58,9 +91,71 @@ func init() {
}

func (peering *ExternalPeering) Default() {
// TODO
if peering.Labels == nil {
peering.Labels = map[string]string{}
}

wiringapi.CleanupFabricLabels(peering.Labels)

peering.Labels[LabelVPC] = peering.Spec.Permit.VPC.Name
peering.Labels[LabelExternal] = peering.Spec.Permit.External.Name

sort.Strings(peering.Spec.Permit.VPC.Subnets)
sort.Slice(peering.Spec.Permit.External.Prefixes, func(i, j int) bool {
return peering.Spec.Permit.External.Prefixes[i].Prefix < peering.Spec.Permit.External.Prefixes[j].Prefix
})
}

func (peering *ExternalPeering) Validate(ctx context.Context, client validation.Client) (admission.Warnings, error) {
return nil, nil // TODO
if peering.Spec.Permit.VPC.Name == "" {
return nil, errors.Errorf("vpc.name is required")
}
if peering.Spec.Permit.External.Name == "" {
return nil, errors.Errorf("external.name is required")
}

for _, permit := range peering.Spec.Permit.External.Prefixes {
if permit.Prefix == "" {
return nil, errors.Errorf("external.prefixes.prefix is required")
}
if permit.Ge > permit.Le {
return nil, errors.Errorf("external.prefixes.ge must be <= external.prefixes.le")
}
if permit.Ge > 32 {
return nil, errors.Errorf("external.prefixes.ge must be <= 32")
}
if permit.Le > 32 {
return nil, errors.Errorf("external.prefixes.le must be <= 32")
}

// TODO add more validation for prefix/ge/le
}

if client != nil {
vpc := &VPC{}
if err := client.Get(ctx, types.NamespacedName{Name: peering.Spec.Permit.VPC.Name, Namespace: peering.Namespace}, vpc); err != nil {
if apierrors.IsNotFound(err) {
return nil, errors.Errorf("vpc %s not found", peering.Spec.Permit.VPC.Name)
}

return nil, errors.Wrapf(err, "failed to read vpc %s", peering.Spec.Permit.VPC.Name) // TODO replace with some internal error to not expose to the user
}

ext := &External{}
if err := client.Get(ctx, types.NamespacedName{Name: peering.Spec.Permit.External.Name, Namespace: peering.Namespace}, ext); err != nil {
if apierrors.IsNotFound(err) {
return nil, errors.Errorf("external %s not found", peering.Spec.Permit.External.Name)
}

return nil, errors.Wrapf(err, "failed to read external %s", peering.Spec.Permit.External.Name) // TODO replace with some internal error to not expose to the user
}

for _, subnet := range peering.Spec.Permit.VPC.Subnets {
if _, exists := vpc.Spec.Subnets[subnet]; !exists {
return nil, errors.Errorf("vpc %s does not have subnet %s", peering.Spec.Permit.VPC.Name, subnet)
}
}
}

return nil, nil
}
1 change: 1 addition & 0 deletions api/vpc/v1alpha2/meta.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ var (
LabelSubnet = LabelName("subnet")
LabelIPv4NS = LabelName("ipv4ns")
LabelVLANNS = LabelName("vlanns")
LabelExternal = LabelName("external")
ListLabelValue = "true"
)

Expand Down
14 changes: 13 additions & 1 deletion api/vpc/v1alpha2/vpc_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ import (
wiringapi "go.githedgehog.com/fabric/api/wiring/v1alpha2"
"go.githedgehog.com/fabric/pkg/manager/validation"
"go.githedgehog.com/fabric/pkg/util/iputil"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
)

Expand Down Expand Up @@ -198,8 +200,18 @@ func (vpc *VPC) Validate(ctx context.Context, client validation.Client, reserved
if client != nil {
// TODO check VLANs
// TODO Can we rely on Validation webhook for croll VPC subnet? if not - main VPC subnet validation should happen in the VPC controller

ipNs := &IPv4Namespace{}
err := client.Get(ctx, types.NamespacedName{Name: vpc.Spec.IPv4Namespace}, ipNs)
if err != nil {
if apierrors.IsNotFound(err) {
return nil, errors.Errorf("IPv4Namespace %s not found", vpc.Spec.IPv4Namespace)
}
return nil, errors.Wrapf(err, "failed to get IPv4Namespace %s", vpc.Spec.IPv4Namespace) // TODO replace with some internal error to not expose to the user
}

vpcs := &VPCList{}
err := client.List(ctx, vpcs, map[string]string{
err = client.List(ctx, vpcs, map[string]string{
LabelIPv4NS: vpc.Spec.IPv4Namespace,
})
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion api/vpc/v1alpha2/vpcattachment_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ type VPCAttachmentStatus struct {

// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:resource:categories=hedgehog;fabric,shortName=vpcattach;attach;va
// +kubebuilder:resource:categories=hedgehog;fabric,shortName=vpcattach
// +kubebuilder:printcolumn:name="VPCSUBNET",type=string,JSONPath=`.spec.subnet`,priority=0
// +kubebuilder:printcolumn:name="Connection",type=string,JSONPath=`.spec.connection`,priority=0
// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`,priority=0
Expand Down
2 changes: 1 addition & 1 deletion api/vpc/v1alpha2/vpcpeering_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ type VPCPeeringStatus struct{}

// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:resource:categories=hedgehog;fabric,shortName=vpcpeer;vp
// +kubebuilder:resource:categories=hedgehog;fabric,shortName=vpcpeer
// +kubebuilder:printcolumn:name="VPC1",type=string,JSONPath=`.metadata.labels.fabric\.githedgehog\.com/vpc1`,priority=0
// +kubebuilder:printcolumn:name="VPC2",type=string,JSONPath=`.metadata.labels.fabric\.githedgehog\.com/vpc2`,priority=0
// +kubebuilder:printcolumn:name="Remote",type=string,JSONPath=`.spec.remote`,priority=0
Expand Down
Loading

0 comments on commit 1345ecb

Please sign in to comment.