Skip to content

Commit 3e98cbf

Browse files
Add basic control plane configuration and cisco nxos reconciliation
1 parent be152fc commit 3e98cbf

File tree

34 files changed

+1411
-249
lines changed

34 files changed

+1411
-249
lines changed

Tiltfile

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,13 @@
55
# Don't track us.
66
analytics_settings(False)
77

8+
update_settings(k8s_upsert_timeout_secs=60)
9+
810
allow_k8s_contexts(['minikube', 'kind-network'])
911

12+
load('ext://cert_manager', 'deploy_cert_manager')
13+
deploy_cert_manager(version='v1.18.2')
14+
1015
docker_build('controller:latest', '.', ignore=['*/*/zz_generated.deepcopy.go', 'config/crd/bases/*'], only=[
1116
'api/', 'cmd/', 'hack/', 'internal/', 'go.mod', 'go.sum', 'Makefile',
1217
])
@@ -20,7 +25,10 @@ k8s_resource('network-operator-controller-manager', resource_deps=['controller-g
2025

2126
# Sample resources with manual trigger mode
2227
k8s_yaml('./config/samples/v1alpha1_device.yaml')
23-
k8s_resource(new_name='leaf1', objects=['leaf1:device'], trigger_mode=TRIGGER_MODE_MANUAL, auto_init=False)
28+
k8s_resource(new_name='credentials', objects=['secret-basic-auth:secret'], trigger_mode=TRIGGER_MODE_MANUAL, auto_init=False)
29+
k8s_resource(new_name='leaf1', objects=['leaf1:device'], resource_deps=['credentials'], trigger_mode=TRIGGER_MODE_MANUAL, auto_init=False)
30+
k8s_resource(new_name='issuer', objects=['network-operator:issuer'], trigger_mode=TRIGGER_MODE_MANUAL, auto_init=False)
31+
k8s_resource(new_name='certificate', objects=['network-operator-ca:certificate'], resource_deps=['issuer'], trigger_mode=TRIGGER_MODE_MANUAL, auto_init=False)
2432

2533
k8s_yaml('./config/samples/v1alpha1_interface.yaml')
2634
k8s_resource(new_name='lo0', objects=['lo0:interface'], trigger_mode=TRIGGER_MODE_MANUAL, auto_init=False)

api/v1alpha1/device_types.go

Lines changed: 103 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
package v1alpha1
55

66
import (
7+
"fmt"
8+
79
corev1 "k8s.io/api/core/v1"
810
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
911
)
@@ -67,24 +69,26 @@ type DeviceSpec struct {
6769
Banner *TemplateSource `json:"banner,omitempty"`
6870
}
6971

72+
func (d *DeviceSpec) Validate() error {
73+
for _, acl := range d.ACL {
74+
if err := acl.Validate(); err != nil {
75+
return err
76+
}
77+
}
78+
return nil
79+
}
80+
7081
type TLS struct {
7182
// The CA certificate to verify the server's identity.
7283
// +required
73-
CA *CertificateAuthority `json:"ca"`
84+
CA *corev1.SecretKeySelector `json:"ca"`
7485

7586
// The client certificate and private key to use for mutual TLS authentication.
7687
// Leave empty if mTLS is not desired.
7788
// +optional
7889
Certificate *CertificateSource `json:"certificate,omitempty"`
7990
}
8091

81-
// CertificateAuthority represents a source for the value of a certificate authority.
82-
type CertificateAuthority struct {
83-
// The secret must contain the following key: 'ca.crt'.
84-
// +required
85-
SecretRef *corev1.SecretReference `json:"secretRef,omitempty"`
86-
}
87-
8892
// Bootstrap defines the configuration for device bootstrap.
8993
type Bootstrap struct {
9094
// Template defines the multiline string template that contains the initial configuration for the device.
@@ -155,6 +159,20 @@ type ACL struct {
155159
Entries []*ACLEntry `json:"entries"`
156160
}
157161

162+
func (acl *ACL) Validate() error {
163+
set := map[int]struct{}{}
164+
for _, entry := range acl.Entries {
165+
if _, exists := set[entry.Sequence]; exists {
166+
return fmt.Errorf("duplicate sequence number %d in ACL %q", entry.Sequence, acl.Name)
167+
}
168+
set[entry.Sequence] = struct{}{}
169+
if err := entry.Validate(); err != nil {
170+
return fmt.Errorf("invalid entry in acl %q: %w", acl.Name, err)
171+
}
172+
}
173+
return nil
174+
}
175+
158176
type ACLEntry struct {
159177
// The sequence number of the ACL entry.
160178
// +required
@@ -164,15 +182,35 @@ type ACLEntry struct {
164182
// +required
165183
Action ACLAction `json:"action"`
166184

185+
// The protocol to match. If not specified, defaults to "ip" (IPv4).
186+
// +kubebuilder:validation:Enum=ahp;eigrp;esp;gre;icmp;igmp;ip;nos;ospf;pcp;pim;tcp;udf;udp
187+
// +kubebuilder:default=ip
188+
// +optional
189+
Protocol string `json:"protocol,omitempty"`
190+
167191
// Source IPv4 address prefix. Use 0.0.0.0/0 to represent 'any'.
168-
// +kubebuilder:validation:Pattern=`^(\d{1,3}\.){3}\d{1,3}\/\d{1,2}$`
169192
// +required
170-
SourceAddress string `json:"sourceAddress"`
193+
SourceAddress IPPrefix `json:"sourceAddress"`
171194

172195
// Destination IPv4 address prefix. Use 0.0.0.0/0 to represent 'any'.
173-
// +kubebuilder:validation:Pattern=`^(\d{1,3}\.){3}\d{1,3}\/\d{1,2}$`
174196
// +required
175-
DestinationAddress string `json:"destinationAddress"`
197+
DestinationAddress IPPrefix `json:"destinationAddress"`
198+
}
199+
200+
func (e *ACLEntry) Validate() error {
201+
if !e.SourceAddress.IsValid() {
202+
return fmt.Errorf("invalid IP prefix: %s", e.SourceAddress.String())
203+
}
204+
if !e.SourceAddress.Addr().Is4() {
205+
return fmt.Errorf("source address must be an IPv4 address: %s", e.SourceAddress.String())
206+
}
207+
if !e.DestinationAddress.IsValid() {
208+
return fmt.Errorf("invalid IP prefix: %s", e.SourceAddress.String())
209+
}
210+
if !e.DestinationAddress.Addr().Is4() {
211+
return fmt.Errorf("destination address must be an IPv4 address: %s", e.DestinationAddress.String())
212+
}
213+
return nil
176214
}
177215

178216
// ACLAction represents the type of action that can be taken by an ACL rule.
@@ -230,7 +268,7 @@ type LogServer struct {
230268

231269
// The destination port number for syslog UDP messages to
232270
// the server. The default is 514.
233-
// +kubebuilder:validation:Default=514
271+
// +kubebuilder:default=514
234272
// +optional
235273
Port int64 `json:"port"`
236274
}
@@ -436,7 +474,7 @@ type CertificateSource struct {
436474
type PasswordSource struct {
437475
// Selects a key of a secret.
438476
// +required
439-
SecretKeyRef *corev1.SecretReference `json:"secretKeyRef,omitempty"`
477+
SecretKeyRef *corev1.SecretKeySelector `json:"secretKeyRef,omitempty"`
440478
}
441479

442480
// DeviceStatus defines the observed state of Device.
@@ -494,6 +532,57 @@ type Device struct {
494532
Status DeviceStatus `json:"status,omitempty"`
495533
}
496534

535+
// GetSecretRefs returns the list of secrets referenced in the [Device] resource.
536+
func (d *Device) GetSecretRefs() []corev1.SecretReference {
537+
refs := []corev1.SecretReference{}
538+
if d.Spec.SecretRef != nil {
539+
refs = append(refs, *d.Spec.SecretRef)
540+
}
541+
if d.Spec.TLS != nil {
542+
refs = append(refs, corev1.SecretReference{Name: d.Spec.TLS.CA.Name})
543+
if d.Spec.TLS.Certificate != nil {
544+
refs = append(refs, *d.Spec.TLS.Certificate.SecretRef)
545+
}
546+
}
547+
if d.Spec.Bootstrap != nil && d.Spec.Bootstrap.Template != nil {
548+
if d.Spec.Bootstrap.Template.SecretRef != nil {
549+
refs = append(refs, corev1.SecretReference{Name: d.Spec.Bootstrap.Template.SecretRef.Name})
550+
}
551+
}
552+
if d.Spec.PKI != nil {
553+
for _, cert := range d.Spec.PKI.Certificates {
554+
if cert.Source != nil && cert.Source.SecretRef != nil {
555+
refs = append(refs, *cert.Source.SecretRef)
556+
}
557+
}
558+
}
559+
for _, user := range d.Spec.User {
560+
refs = append(refs, corev1.SecretReference{Name: user.Password.SecretKeyRef.Name})
561+
}
562+
for i := range refs {
563+
if refs[i].Namespace == "" {
564+
refs[i].Namespace = d.Namespace
565+
}
566+
}
567+
return refs
568+
}
569+
570+
// GetConfigMapRefs returns the list of configmaps referenced in the [Device] resource.
571+
func (d *Device) GetConfigMapRefs() []corev1.ObjectReference {
572+
refs := []corev1.ObjectReference{}
573+
if d.Spec.Bootstrap != nil && d.Spec.Bootstrap.Template != nil {
574+
if d.Spec.Bootstrap.Template.ConfigMapRef != nil {
575+
refs = append(refs, corev1.ObjectReference{Name: d.Spec.Bootstrap.Template.ConfigMapRef.Name})
576+
}
577+
}
578+
for i := range refs {
579+
if refs[i].Namespace == "" {
580+
refs[i].Namespace = d.Namespace
581+
}
582+
}
583+
return refs
584+
}
585+
497586
// +kubebuilder:object:root=true
498587

499588
// DeviceList contains a list of Device.

api/v1alpha1/groupversion_info.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,3 +67,15 @@ const (
6767
// AllResourcesReadyReason indicates that all resources owned by the resource are ready.
6868
AllResourcesReadyReason = "AllResourcesReady"
6969
)
70+
71+
// Device reasons that are used specifically for Device objects.
72+
const (
73+
// UnreachableReason indicates that the device is not reachable over the network.
74+
DeviceUnreachableReason string = "Unreachable"
75+
76+
// UnsupportedReason indicates that the device platform is not supported.
77+
DeviceUnsupportedReason string = "Unsupported"
78+
79+
// UnauthenticatedReason indicates that the provided device credentials are not valid.
80+
DeviceUnauthenticatedReason string = "Unauthenticated"
81+
)

api/v1alpha1/prefix_types.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package v1alpha1
2+
3+
import (
4+
"encoding/json"
5+
"net/netip"
6+
)
7+
8+
// IPPrefix represents an IP prefix in CIDR notation.
9+
// It is used to define a range of IP addresses in a network.
10+
//
11+
// +kubebuilder:validation:Type=string
12+
// +kubebuilder:validation:Pattern=`^(\d{1,3}\.){3}\d{1,3}\/\d{1,2}$`
13+
// +kubebuilder:validation:Example="192.168.1.0/24"
14+
// +kubebuilder:validation:Example="2001:db8::/32"
15+
// +kubebuilder:object:generate=false
16+
type IPPrefix struct {
17+
netip.Prefix `json:"-"`
18+
}
19+
20+
func ParsePrefix(s string) (IPPrefix, error) {
21+
prefix, err := netip.ParsePrefix(s)
22+
if err != nil {
23+
return IPPrefix{}, err
24+
}
25+
return IPPrefix{prefix}, nil
26+
}
27+
28+
func MustParsePrefix(s string) IPPrefix {
29+
prefix := netip.MustParsePrefix(s)
30+
return IPPrefix{prefix}
31+
}
32+
33+
// IsZero reports whether p represents the zero value
34+
func (p IPPrefix) IsZero() bool {
35+
return !p.IsValid()
36+
}
37+
38+
// MarshalJSON implements [json.Marshaler].
39+
func (p IPPrefix) MarshalJSON() ([]byte, error) {
40+
if !p.IsValid() {
41+
return []byte("null"), nil
42+
}
43+
return json.Marshal(p.String())
44+
}
45+
46+
// UnmarshalJSON implements [json.Unmarshaler].
47+
func (p *IPPrefix) UnmarshalJSON(data []byte) error {
48+
var str string
49+
if err := json.Unmarshal(data, &str); err != nil {
50+
return err
51+
}
52+
if str == "" || str == "null" {
53+
*p = IPPrefix{}
54+
return nil
55+
}
56+
prefix, err := netip.ParsePrefix(str)
57+
if err != nil {
58+
return err
59+
}
60+
*p = IPPrefix{prefix}
61+
return nil
62+
}
63+
64+
// DeepCopyInto copies all properties of this object into another object of the same type
65+
func (in *IPPrefix) DeepCopyInto(out *IPPrefix) {
66+
*out = *in
67+
}
68+
69+
// DeepCopy creates a deep copy of the IPPrefix
70+
func (in *IPPrefix) DeepCopy() *IPPrefix {
71+
if in == nil {
72+
return nil
73+
}
74+
out := new(IPPrefix)
75+
in.DeepCopyInto(out)
76+
return out
77+
}

api/v1alpha1/zz_generated.deepcopy.go

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

cmd/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import (
3535
"sigs.k8s.io/controller-runtime/pkg/webhook"
3636

3737
// Import all supported provider implementations.
38+
_ "github.com/ironcore-dev/network-operator/internal/provider/cisco/nxos"
3839
_ "github.com/ironcore-dev/network-operator/internal/provider/openconfig"
3940

4041
"github.com/ironcore-dev/network-operator/api/v1alpha1"

0 commit comments

Comments
 (0)