From 1345ecb7c3dc117e914a3dccdca34c99c06543e1 Mon Sep 17 00:00:00 2001 From: Sergei Lukianov Date: Thu, 7 Dec 2023 23:49:36 -0800 Subject: [PATCH] [api] add initial External API with basic validation --- api/vpc/v1alpha2/external_types.go | 57 +++++- api/vpc/v1alpha2/externalattachment_types.go | 90 +++++++++- api/vpc/v1alpha2/externalpeering_types.go | 107 +++++++++++- api/vpc/v1alpha2/meta.go | 1 + api/vpc/v1alpha2/vpc_types.go | 14 +- api/vpc/v1alpha2/vpcattachment_types.go | 2 +- api/vpc/v1alpha2/vpcpeering_types.go | 2 +- api/vpc/v1alpha2/zz_generated.deepcopy.go | 107 +++++++++++- api/wiring/v1alpha2/connection_types.go | 20 +++ api/wiring/v1alpha2/zz_generated.deepcopy.go | 37 ++++ cmd/hhfctl/main.go | 45 +++++ .../bases/agent.githedgehog.com_agents.yaml | 11 ++ ...c.githedgehog.com_externalattachments.yaml | 54 +++++- .../vpc.githedgehog.com_externalpeerings.yaml | 56 +++++- .../bases/vpc.githedgehog.com_externals.yaml | 28 ++- .../vpc.githedgehog.com_vpcattachments.yaml | 2 - .../vpc.githedgehog.com_vpcpeerings.yaml | 1 - .../wiring.githedgehog.com_connections.yaml | 11 ++ config/rbac/role.yaml | 48 ++++++ docs/api.md | 162 ++++++++++++++++++ pkg/ctrl/agent/agent_ctrl.go | 9 + pkg/hhfctl/external.go | 72 ++++++++ 22 files changed, 908 insertions(+), 28 deletions(-) create mode 100644 pkg/hhfctl/external.go diff --git a/api/vpc/v1alpha2/external_types.go b/api/vpc/v1alpha2/external_types.go index 812d411d..e1f0ffab 100644 --- a/api/vpc/v1alpha2/external_types.go +++ b/api/vpc/v1alpha2/external_types.go @@ -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"` @@ -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 } diff --git a/api/vpc/v1alpha2/externalattachment_types.go b/api/vpc/v1alpha2/externalattachment_types.go index 8003a117..acfedd8b 100644 --- a/api/vpc/v1alpha2/externalattachment_types.go +++ b/api/vpc/v1alpha2/externalattachment_types.go @@ -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"` @@ -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 } diff --git a/api/vpc/v1alpha2/externalpeering_types.go b/api/vpc/v1alpha2/externalpeering_types.go index 901d43f2..1c1576a7 100644 --- a/api/vpc/v1alpha2/externalpeering_types.go +++ b/api/vpc/v1alpha2/externalpeering_types.go @@ -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"` @@ -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 } diff --git a/api/vpc/v1alpha2/meta.go b/api/vpc/v1alpha2/meta.go index a8a13606..eb143e5f 100644 --- a/api/vpc/v1alpha2/meta.go +++ b/api/vpc/v1alpha2/meta.go @@ -8,6 +8,7 @@ var ( LabelSubnet = LabelName("subnet") LabelIPv4NS = LabelName("ipv4ns") LabelVLANNS = LabelName("vlanns") + LabelExternal = LabelName("external") ListLabelValue = "true" ) diff --git a/api/vpc/v1alpha2/vpc_types.go b/api/vpc/v1alpha2/vpc_types.go index 4c5a12d4..8bd974db 100644 --- a/api/vpc/v1alpha2/vpc_types.go +++ b/api/vpc/v1alpha2/vpc_types.go @@ -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" ) @@ -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 { diff --git a/api/vpc/v1alpha2/vpcattachment_types.go b/api/vpc/v1alpha2/vpcattachment_types.go index d10b5cc5..0b78d5fd 100644 --- a/api/vpc/v1alpha2/vpcattachment_types.go +++ b/api/vpc/v1alpha2/vpcattachment_types.go @@ -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 diff --git a/api/vpc/v1alpha2/vpcpeering_types.go b/api/vpc/v1alpha2/vpcpeering_types.go index 8ee10025..a27b8c7b 100644 --- a/api/vpc/v1alpha2/vpcpeering_types.go +++ b/api/vpc/v1alpha2/vpcpeering_types.go @@ -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 diff --git a/api/vpc/v1alpha2/zz_generated.deepcopy.go b/api/vpc/v1alpha2/zz_generated.deepcopy.go index 634be0ad..88331117 100644 --- a/api/vpc/v1alpha2/zz_generated.deepcopy.go +++ b/api/vpc/v1alpha2/zz_generated.deepcopy.go @@ -111,9 +111,26 @@ func (in *ExternalAttachmentList) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ExternalAttachmentNeighbor) DeepCopyInto(out *ExternalAttachmentNeighbor) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalAttachmentNeighbor. +func (in *ExternalAttachmentNeighbor) DeepCopy() *ExternalAttachmentNeighbor { + if in == nil { + return nil + } + out := new(ExternalAttachmentNeighbor) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ExternalAttachmentSpec) DeepCopyInto(out *ExternalAttachmentSpec) { *out = *in + out.Switch = in.Switch + out.Neighbor = in.Neighbor } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalAttachmentSpec. @@ -141,6 +158,21 @@ func (in *ExternalAttachmentStatus) DeepCopy() *ExternalAttachmentStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ExternalAttachmentSwitch) DeepCopyInto(out *ExternalAttachmentSwitch) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalAttachmentSwitch. +func (in *ExternalAttachmentSwitch) DeepCopy() *ExternalAttachmentSwitch { + if in == nil { + return nil + } + out := new(ExternalAttachmentSwitch) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ExternalList) DeepCopyInto(out *ExternalList) { *out = *in @@ -178,7 +210,7 @@ func (in *ExternalPeering) DeepCopyInto(out *ExternalPeering) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec + in.Spec.DeepCopyInto(&out.Spec) out.Status = in.Status } @@ -235,6 +267,7 @@ func (in *ExternalPeeringList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ExternalPeeringSpec) DeepCopyInto(out *ExternalPeeringSpec) { *out = *in + in.Permit.DeepCopyInto(&out.Permit) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalPeeringSpec. @@ -247,6 +280,78 @@ func (in *ExternalPeeringSpec) DeepCopy() *ExternalPeeringSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ExternalPeeringSpecExternal) DeepCopyInto(out *ExternalPeeringSpecExternal) { + *out = *in + if in.Prefixes != nil { + in, out := &in.Prefixes, &out.Prefixes + *out = make([]ExternalPeeringSpecPrefix, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalPeeringSpecExternal. +func (in *ExternalPeeringSpecExternal) DeepCopy() *ExternalPeeringSpecExternal { + if in == nil { + return nil + } + out := new(ExternalPeeringSpecExternal) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ExternalPeeringSpecPermit) DeepCopyInto(out *ExternalPeeringSpecPermit) { + *out = *in + in.VPC.DeepCopyInto(&out.VPC) + in.External.DeepCopyInto(&out.External) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalPeeringSpecPermit. +func (in *ExternalPeeringSpecPermit) DeepCopy() *ExternalPeeringSpecPermit { + if in == nil { + return nil + } + out := new(ExternalPeeringSpecPermit) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ExternalPeeringSpecPrefix) DeepCopyInto(out *ExternalPeeringSpecPrefix) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalPeeringSpecPrefix. +func (in *ExternalPeeringSpecPrefix) DeepCopy() *ExternalPeeringSpecPrefix { + if in == nil { + return nil + } + out := new(ExternalPeeringSpecPrefix) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ExternalPeeringSpecVPC) DeepCopyInto(out *ExternalPeeringSpecVPC) { + *out = *in + if in.Subnets != nil { + in, out := &in.Subnets, &out.Subnets + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalPeeringSpecVPC. +func (in *ExternalPeeringSpecVPC) DeepCopy() *ExternalPeeringSpecVPC { + if in == nil { + return nil + } + out := new(ExternalPeeringSpecVPC) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ExternalPeeringStatus) DeepCopyInto(out *ExternalPeeringStatus) { *out = *in diff --git a/api/wiring/v1alpha2/connection_types.go b/api/wiring/v1alpha2/connection_types.go index 4ad91269..f2232c99 100644 --- a/api/wiring/v1alpha2/connection_types.go +++ b/api/wiring/v1alpha2/connection_types.go @@ -40,6 +40,7 @@ const ( CONNECTION_TYPE_NAT = "nat" CONNECTION_TYPE_FABRIC = "fabric" CONNECTION_TYPE_VPC_LOOPBACK = "vpc-loopback" + CONNECTION_EXTERNAL = "external" ) // NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. @@ -145,6 +146,14 @@ type ConnVPCLoopback struct { Links []SwitchToSwitchLink `json:"links,omitempty"` } +type ConnExternalLink struct { + Switch BasePortName `json:"switch,omitempty"` +} + +type ConnExternal struct { + Link ConnExternalLink `json:"link,omitempty"` +} + // ConnectionSpec defines the desired state of Connection type ConnectionSpec struct { Unbundled *ConnUnbundled `json:"unbundled,omitempty"` @@ -155,6 +164,7 @@ type ConnectionSpec struct { NAT *ConnNAT `json:"nat,omitempty"` Fabric *ConnFabric `json:"fabric,omitempty"` VPCLoopback *ConnVPCLoopback `json:"vpcLoopback,omitempty"` + External *ConnExternal `json:"external,omitempty"` } // ConnectionStatus defines the observed state of Connection @@ -286,6 +296,9 @@ func (c *ConnectionSpec) GenerateName() string { } else if c.VPCLoopback != nil { role = "vpc-loopback" left = c.VPCLoopback.Links[0].Switch1.DeviceName() + } else if c.External != nil { + role = "external" + left = c.External.Link.Switch.DeviceName() } if left != "" && role != "" { @@ -335,6 +348,8 @@ func (c *ConnectionSpec) ConnectionLabels() map[string]string { labels[LabelConnectionType] = CONNECTION_TYPE_FABRIC } else if c.VPCLoopback != nil { labels[LabelConnectionType] = CONNECTION_TYPE_VPC_LOOPBACK + } else if c.External != nil { + labels[LabelConnectionType] = CONNECTION_EXTERNAL } return labels @@ -491,6 +506,11 @@ func (s *ConnectionSpec) Endpoints() ([]string, []string, []string, map[string]s if len(ports) != 2*len(s.VPCLoopback.Links) { return nil, nil, nil, nil, errors.Errorf("unique ports must be used for fabric connection") } + } else if s.External != nil { + nonNills++ + + switches[s.External.Link.Switch.DeviceName()] = struct{}{} + ports[s.External.Link.Switch.PortName()] = struct{}{} } if nonNills != 1 { diff --git a/api/wiring/v1alpha2/zz_generated.deepcopy.go b/api/wiring/v1alpha2/zz_generated.deepcopy.go index 9a2dbdbc..fabddcec 100644 --- a/api/wiring/v1alpha2/zz_generated.deepcopy.go +++ b/api/wiring/v1alpha2/zz_generated.deepcopy.go @@ -84,6 +84,38 @@ func (in *ConnBundled) DeepCopy() *ConnBundled { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ConnExternal) DeepCopyInto(out *ConnExternal) { + *out = *in + out.Link = in.Link +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConnExternal. +func (in *ConnExternal) DeepCopy() *ConnExternal { + if in == nil { + return nil + } + out := new(ConnExternal) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ConnExternalLink) DeepCopyInto(out *ConnExternalLink) { + *out = *in + out.Switch = in.Switch +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConnExternalLink. +func (in *ConnExternalLink) DeepCopy() *ConnExternalLink { + if in == nil { + return nil + } + out := new(ConnExternalLink) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ConnFabric) DeepCopyInto(out *ConnFabric) { *out = *in @@ -418,6 +450,11 @@ func (in *ConnectionSpec) DeepCopyInto(out *ConnectionSpec) { *out = new(ConnVPCLoopback) (*in).DeepCopyInto(*out) } + if in.External != nil { + in, out := &in.External, &out.External + *out = new(ConnExternal) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConnectionSpec. diff --git a/cmd/hhfctl/main.go b/cmd/hhfctl/main.go index 6669efa0..5f634cc3 100644 --- a/cmd/hhfctl/main.go +++ b/cmd/hhfctl/main.go @@ -318,6 +318,7 @@ func main() { Flags: []cli.Flag{ verboseFlag, nameFlag, + printYamlFlag, }, Before: func(cCtx *cli.Context) error { return setupLogger(verbose) @@ -330,6 +331,50 @@ func main() { }, }, }, + { + Name: "external", + Usage: "External commands", + Flags: []cli.Flag{ + verboseFlag, + }, + Subcommands: []*cli.Command{ + { + Name: "create", + Usage: "Create External", + Flags: []cli.Flag{ + verboseFlag, + nameFlag, + printYamlFlag, + &cli.StringFlag{ + Name: "ipv4-namespace", + Aliases: []string{"ipns"}, + Usage: "ipv4 namespace", + }, + &cli.StringFlag{ + Name: "inbound-community", + Aliases: []string{"in"}, + Usage: "inbound community", + }, + &cli.StringFlag{ + Name: "outbound-community", + Aliases: []string{"out"}, + Usage: "outbound community", + }, + }, + Before: func(cCtx *cli.Context) error { + return setupLogger(verbose) + }, + Action: func(cCtx *cli.Context) error { + return hhfctl.ExternalCreate(ctx, printYaml, &hhfctl.ExternalCreateOptions{ + Name: name, + IPv4Namespace: cCtx.String("ipv4-namespace"), + InboundCommunity: cCtx.String("inbound-community"), + OutboundCommunity: cCtx.String("outbound-community"), + }) + }, + }, + }, + }, }, } diff --git a/config/crd/bases/agent.githedgehog.com_agents.yaml b/config/crd/bases/agent.githedgehog.com_agents.yaml index 1002932f..d1f9337c 100644 --- a/config/crd/bases/agent.githedgehog.com_agents.yaml +++ b/config/crd/bases/agent.githedgehog.com_agents.yaml @@ -123,6 +123,17 @@ spec: type: object type: array type: object + external: + properties: + link: + properties: + switch: + properties: + port: + type: string + type: object + type: object + type: object fabric: properties: links: diff --git a/config/crd/bases/vpc.githedgehog.com_externalattachments.yaml b/config/crd/bases/vpc.githedgehog.com_externalattachments.yaml index 074e5f7d..32fdc1ae 100644 --- a/config/crd/bases/vpc.githedgehog.com_externalattachments.yaml +++ b/config/crd/bases/vpc.githedgehog.com_externalattachments.yaml @@ -8,13 +8,45 @@ metadata: spec: group: vpc.githedgehog.com names: + categories: + - hedgehog + - fabric + - external kind: ExternalAttachment listKind: ExternalAttachmentList plural: externalattachments + shortNames: + - extattach singular: externalattachment scope: Namespaced versions: - - name: v1alpha2 + - additionalPrinterColumns: + - jsonPath: .spec.external + name: External + type: string + - jsonPath: .spec.connection + name: Connection + type: string + - jsonPath: .spec.switch.vlan + name: SwVLAN + priority: 1 + type: string + - jsonPath: .spec.switch.ip + name: SwIP + priority: 1 + type: string + - jsonPath: .spec.neighbor.asn + name: NeighASN + priority: 1 + type: string + - jsonPath: .spec.neighbor.ip + name: NeighIP + priority: 1 + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha2 schema: openAPIV3Schema: description: ExternalAttachment is the Schema for the externalattachments @@ -34,6 +66,26 @@ spec: type: object spec: description: ExternalAttachmentSpec defines the desired state of ExternalAttachment + properties: + connection: + type: string + external: + type: string + neighbor: + properties: + asn: + format: int32 + type: integer + ip: + type: string + type: object + switch: + properties: + ip: + type: string + vlan: + type: integer + type: object type: object status: description: ExternalAttachmentStatus defines the observed state of ExternalAttachment diff --git a/config/crd/bases/vpc.githedgehog.com_externalpeerings.yaml b/config/crd/bases/vpc.githedgehog.com_externalpeerings.yaml index 02913d69..5f0e7983 100644 --- a/config/crd/bases/vpc.githedgehog.com_externalpeerings.yaml +++ b/config/crd/bases/vpc.githedgehog.com_externalpeerings.yaml @@ -8,13 +8,38 @@ metadata: spec: group: vpc.githedgehog.com names: + categories: + - hedgehog + - fabric + - external kind: ExternalPeering listKind: ExternalPeeringList plural: externalpeerings + shortNames: + - extpeering + - extpeer singular: externalpeering scope: Namespaced versions: - - name: v1alpha2 + - additionalPrinterColumns: + - jsonPath: .spec.permit.vpc.name + name: VPC + type: string + - jsonPath: .spec.permit.vpc.subnets + name: VPCSubnets + priority: 1 + type: string + - jsonPath: .spec.permit.external.name + name: External + type: string + - jsonPath: .spec.permit.external.prefixes + name: ExtPrefixes + priority: 1 + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha2 schema: openAPIV3Schema: description: ExternalPeering is the Schema for the externalpeerings API @@ -33,6 +58,35 @@ spec: type: object spec: description: ExternalPeeringSpec defines the desired state of ExternalPeering + properties: + permit: + properties: + external: + properties: + name: + type: string + prefixes: + items: + properties: + ge: + type: integer + le: + type: integer + prefix: + type: string + type: object + type: array + type: object + vpc: + properties: + name: + type: string + subnets: + items: + type: string + type: array + type: object + type: object type: object status: description: ExternalPeeringStatus defines the observed state of ExternalPeering diff --git a/config/crd/bases/vpc.githedgehog.com_externals.yaml b/config/crd/bases/vpc.githedgehog.com_externals.yaml index 278d1cf7..0bbc8b70 100644 --- a/config/crd/bases/vpc.githedgehog.com_externals.yaml +++ b/config/crd/bases/vpc.githedgehog.com_externals.yaml @@ -8,13 +8,32 @@ metadata: spec: group: vpc.githedgehog.com names: + categories: + - hedgehog + - fabric + - external kind: External listKind: ExternalList plural: externals + shortNames: + - ext singular: external scope: Namespaced versions: - - name: v1alpha2 + - additionalPrinterColumns: + - jsonPath: .spec.ipv4Namespace + name: IPv4NS + type: string + - jsonPath: .spec.inboundCommunity + name: InComm + type: string + - jsonPath: .spec.outboundCommunity + name: OutComm + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha2 schema: openAPIV3Schema: description: External is the Schema for the externals API @@ -33,6 +52,13 @@ spec: type: object spec: description: ExternalSpec defines the desired state of External + properties: + inboundCommunity: + type: string + ipv4Namespace: + type: string + outboundCommunity: + type: string type: object status: description: ExternalStatus defines the observed state of External diff --git a/config/crd/bases/vpc.githedgehog.com_vpcattachments.yaml b/config/crd/bases/vpc.githedgehog.com_vpcattachments.yaml index e7d21787..f2f86292 100644 --- a/config/crd/bases/vpc.githedgehog.com_vpcattachments.yaml +++ b/config/crd/bases/vpc.githedgehog.com_vpcattachments.yaml @@ -16,8 +16,6 @@ spec: plural: vpcattachments shortNames: - vpcattach - - attach - - va singular: vpcattachment scope: Namespaced versions: diff --git a/config/crd/bases/vpc.githedgehog.com_vpcpeerings.yaml b/config/crd/bases/vpc.githedgehog.com_vpcpeerings.yaml index c8ebb092..7c8de0f4 100644 --- a/config/crd/bases/vpc.githedgehog.com_vpcpeerings.yaml +++ b/config/crd/bases/vpc.githedgehog.com_vpcpeerings.yaml @@ -16,7 +16,6 @@ spec: plural: vpcpeerings shortNames: - vpcpeer - - vp singular: vpcpeering scope: Namespaced versions: diff --git a/config/crd/bases/wiring.githedgehog.com_connections.yaml b/config/crd/bases/wiring.githedgehog.com_connections.yaml index cf5083ba..6eb0b1ab 100644 --- a/config/crd/bases/wiring.githedgehog.com_connections.yaml +++ b/config/crd/bases/wiring.githedgehog.com_connections.yaml @@ -65,6 +65,17 @@ spec: type: object type: array type: object + external: + properties: + link: + properties: + switch: + properties: + port: + type: string + type: object + type: object + type: object fabric: properties: links: diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 358060a0..56e0d6dc 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -150,6 +150,54 @@ rules: - patch - update - watch +- apiGroups: + - vpc.githedgehog.com + resources: + - externalattachments + verbs: + - get + - list + - watch +- apiGroups: + - vpc.githedgehog.com + resources: + - externalattachments/status + verbs: + - get + - patch + - update +- apiGroups: + - vpc.githedgehog.com + resources: + - externalpeerings + verbs: + - get + - list + - watch +- apiGroups: + - vpc.githedgehog.com + resources: + - externalpeerings/status + verbs: + - get + - patch + - update +- apiGroups: + - vpc.githedgehog.com + resources: + - externals + verbs: + - get + - list + - watch +- apiGroups: + - vpc.githedgehog.com + resources: + - externals/status + verbs: + - get + - patch + - update - apiGroups: - vpc.githedgehog.com resources: diff --git a/docs/api.md b/docs/api.md index 15187e6b..763a9b30 100644 --- a/docs/api.md +++ b/docs/api.md @@ -395,10 +395,55 @@ ExternalAttachment is the Schema for the externalattachments API | `status` _[ExternalAttachmentStatus](#externalattachmentstatus)_ | | +#### ExternalAttachmentNeighbor + +_Appears in:_ +- [ExternalAttachmentSpec](#externalattachmentspec) + +| Field | Description | +| --- | --- | +| `asn` _integer_ | | +| `ip` _string_ | | + + +#### ExternalAttachmentSpec + + + +ExternalAttachmentSpec defines the desired state of ExternalAttachment + +_Appears in:_ +- [ExternalAttachment](#externalattachment) + +| Field | Description | +| --- | --- | +| `external` _string_ | | +| `connection` _string_ | | +| `switch` _[ExternalAttachmentSwitch](#externalattachmentswitch)_ | | +| `neighbor` _[ExternalAttachmentNeighbor](#externalattachmentneighbor)_ | | + + + + +#### ExternalAttachmentSwitch + + + + + +_Appears in:_ +- [ExternalAttachmentSpec](#externalattachmentspec) + +| Field | Description | +| --- | --- | +| `vlan` _integer_ | | +| `ip` _string_ | | + + #### ExternalPeering @@ -416,11 +461,98 @@ ExternalPeering is the Schema for the externalpeerings API | `status` _[ExternalPeeringStatus](#externalpeeringstatus)_ | | +#### ExternalPeeringSpec + + + +ExternalPeeringSpec defines the desired state of ExternalPeering + +_Appears in:_ +- [ExternalPeering](#externalpeering) + +| Field | Description | +| --- | --- | +| `permit` _[ExternalPeeringSpecPermit](#externalpeeringspecpermit)_ | | + + +#### ExternalPeeringSpecExternal +_Appears in:_ +- [ExternalPeeringSpecPermit](#externalpeeringspecpermit) + +| Field | Description | +| --- | --- | +| `name` _string_ | | +| `prefixes` _[ExternalPeeringSpecPrefix](#externalpeeringspecprefix) array_ | | + + +#### ExternalPeeringSpecPermit + + + + + +_Appears in:_ +- [ExternalPeeringSpec](#externalpeeringspec) + +| Field | Description | +| --- | --- | +| `vpc` _[ExternalPeeringSpecVPC](#externalpeeringspecvpc)_ | | +| `external` _[ExternalPeeringSpecExternal](#externalpeeringspecexternal)_ | | + + +#### ExternalPeeringSpecPrefix + + + + + +_Appears in:_ +- [ExternalPeeringSpecExternal](#externalpeeringspecexternal) + +| Field | Description | +| --- | --- | +| `prefix` _string_ | | +| `ge` _integer_ | | +| `le` _integer_ | | + + +#### ExternalPeeringSpecVPC + + + + + +_Appears in:_ +- [ExternalPeeringSpecPermit](#externalpeeringspecpermit) + +| Field | Description | +| --- | --- | +| `name` _string_ | | +| `subnets` _string array_ | | + + + + +#### ExternalSpec + + + +ExternalSpec defines the desired state of External + +_Appears in:_ +- [External](#external) + +| Field | Description | +| --- | --- | +| `ipv4Namespace` _string_ | | +| `inboundCommunity` _string_ | | +| `outboundCommunity` _string_ | | + @@ -650,6 +782,7 @@ Package v1alpha2 contains API Schema definitions for the wiring v1alpha2 API gro _Appears in:_ +- [ConnExternalLink](#connexternallink) - [ConnFabricLinkSwitch](#connfabriclinkswitch) - [ConnMgmtLinkServer](#connmgmtlinkserver) - [ConnMgmtLinkSwitch](#connmgmtlinkswitch) @@ -677,6 +810,34 @@ _Appears in:_ | `links` _[ServerToSwitchLink](#servertoswitchlink) array_ | | +#### ConnExternal + + + + + +_Appears in:_ +- [ConnectionSpec](#connectionspec) + +| Field | Description | +| --- | --- | +| `link` _[ConnExternalLink](#connexternallink)_ | | + + +#### ConnExternalLink + + + + + +_Appears in:_ +- [ConnExternal](#connexternal) + +| Field | Description | +| --- | --- | +| `switch` _[BasePortName](#baseportname)_ | | + + #### ConnFabric @@ -909,6 +1070,7 @@ _Appears in:_ | `nat` _[ConnNAT](#connnat)_ | | | `fabric` _[ConnFabric](#connfabric)_ | | | `vpcLoopback` _[ConnVPCLoopback](#connvpcloopback)_ | | +| `external` _[ConnExternal](#connexternal)_ | | diff --git a/pkg/ctrl/agent/agent_ctrl.go b/pkg/ctrl/agent/agent_ctrl.go index 8bf5141b..b7c8d60e 100644 --- a/pkg/ctrl/agent/agent_ctrl.go +++ b/pkg/ctrl/agent/agent_ctrl.go @@ -153,6 +153,15 @@ func (r *AgentReconciler) enqueueAllSwitches(ctx context.Context, obj client.Obj //+kubebuilder:rbac:groups=vpc.githedgehog.com,resources=ipv4namespaces,verbs=get;list;watch //+kubebuilder:rbac:groups=vpc.githedgehog.com,resources=ipv4namespaces/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=vpc.githedgehog.com,resources=externals,verbs=get;list;watch +//+kubebuilder:rbac:groups=vpc.githedgehog.com,resources=externals/status,verbs=get;update;patch + +//+kubebuilder:rbac:groups=vpc.githedgehog.com,resources=externalattachments,verbs=get;list;watch +//+kubebuilder:rbac:groups=vpc.githedgehog.com,resources=externalattachments/status,verbs=get;update;patch + +//+kubebuilder:rbac:groups=vpc.githedgehog.com,resources=externalpeerings,verbs=get;list;watch +//+kubebuilder:rbac:groups=vpc.githedgehog.com,resources=externalpeerings/status,verbs=get;update;patch + //+kubebuilder:rbac:groups=core,resources=serviceaccounts,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=core,resources=secrets,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=roles,verbs=get;list;watch;create;update;patch;delete diff --git a/pkg/hhfctl/external.go b/pkg/hhfctl/external.go new file mode 100644 index 00000000..3e4d5156 --- /dev/null +++ b/pkg/hhfctl/external.go @@ -0,0 +1,72 @@ +package hhfctl + +import ( + "context" + "fmt" + "log/slog" + + "github.com/pkg/errors" + vpcapi "go.githedgehog.com/fabric/api/vpc/v1alpha2" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/yaml" +) + +type ExternalCreateOptions struct { + Name string + IPv4Namespace string + InboundCommunity string + OutboundCommunity string +} + +func ExternalCreate(ctx context.Context, printYaml bool, options *ExternalCreateOptions) error { + ext := &vpcapi.External{ + ObjectMeta: metav1.ObjectMeta{ + Name: options.Name, + Namespace: "default", // TODO ns + }, + Spec: vpcapi.ExternalSpec{ + IPv4Namespace: options.IPv4Namespace, + InboundCommunity: options.InboundCommunity, + OutboundCommunity: options.OutboundCommunity, + }, + } + + kube, err := kubeClient() + if err != nil { + return errors.Wrap(err, "cannot create kube client") + } + + ext.Default() + warnings, err := ext.Validate(ctx /* validation.WithCtrlRuntime(kube) */, nil) + if err != nil { + slog.Warn("Validation", "error", err) + return errors.Errorf("validation failed") + } + if warnings != nil { + slog.Warn("Validation", "warnings", warnings) + } + + err = kube.Create(ctx, ext) + if err != nil { + return errors.Wrap(err, "cannot create external") + } + + slog.Info("External created", "name", ext.Name) + + if printYaml { + ext.ObjectMeta.ManagedFields = nil + ext.ObjectMeta.Generation = 0 + ext.ObjectMeta.ResourceVersion = "" + + out, err := yaml.Marshal(ext) + if err != nil { + return errors.Wrap(err, "cannot marshal ext") + } + + fmt.Println(string(out)) + } + + return nil +} + +type ExternalAttachCreateOptions struct{}