diff --git a/go/nautobotop/api/v1alpha1/nautobot_types.go b/go/nautobotop/api/v1alpha1/nautobot_types.go index dea14d907..4ee115e94 100644 --- a/go/nautobotop/api/v1alpha1/nautobot_types.go +++ b/go/nautobotop/api/v1alpha1/nautobot_types.go @@ -41,10 +41,16 @@ type NautobotSpec struct { RackGroupRef []ConfigMapRef `json:"rackGroupRef,omitempty"` RackRef []ConfigMapRef `json:"rackRef,omitempty"` VlanGroupRef []ConfigMapRef `json:"vlanGroupRef,omitempty"` + VlanRef []ConfigMapRef `json:"vlanRef,omitempty"` + PrefixRef []ConfigMapRef `json:"prefixRef,omitempty"` ClusterTypeRef []ConfigMapRef `json:"clusterTypeRef,omitempty"` ClusterGroupRef []ConfigMapRef `json:"clusterGroupRef,omitempty"` ClusterRef []ConfigMapRef `json:"clusterRef,omitempty"` NamespaceRef []ConfigMapRef `json:"namespaceRef,omitempty"` + RirRef []ConfigMapRef `json:"rirRef,omitempty"` + RoleRef []ConfigMapRef `json:"roleRef,omitempty"` + TenantGroupRef []ConfigMapRef `json:"tenantGroupRef,omitempty"` + TenantRef []ConfigMapRef `json:"tenantRef,omitempty"` } // NautobotStatus defines the observed state of Nautobot. diff --git a/go/nautobotop/api/v1alpha1/zz_generated.deepcopy.go b/go/nautobotop/api/v1alpha1/zz_generated.deepcopy.go index a31b8a624..92f3841fb 100644 --- a/go/nautobotop/api/v1alpha1/zz_generated.deepcopy.go +++ b/go/nautobotop/api/v1alpha1/zz_generated.deepcopy.go @@ -166,6 +166,20 @@ func (in *NautobotSpec) DeepCopyInto(out *NautobotSpec) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.VlanRef != nil { + in, out := &in.VlanRef, &out.VlanRef + *out = make([]ConfigMapRef, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.PrefixRef != nil { + in, out := &in.PrefixRef, &out.PrefixRef + *out = make([]ConfigMapRef, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } if in.ClusterTypeRef != nil { in, out := &in.ClusterTypeRef, &out.ClusterTypeRef *out = make([]ConfigMapRef, len(*in)) @@ -194,6 +208,34 @@ func (in *NautobotSpec) DeepCopyInto(out *NautobotSpec) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.RirRef != nil { + in, out := &in.RirRef, &out.RirRef + *out = make([]ConfigMapRef, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.RoleRef != nil { + in, out := &in.RoleRef, &out.RoleRef + *out = make([]ConfigMapRef, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.TenantGroupRef != nil { + in, out := &in.TenantGroupRef, &out.TenantGroupRef + *out = make([]ConfigMapRef, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.TenantRef != nil { + in, out := &in.TenantRef, &out.TenantRef + *out = make([]ConfigMapRef, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NautobotSpec. diff --git a/go/nautobotop/config/crd/bases/sync.rax.io_nautobots.yaml b/go/nautobotop/config/crd/bases/sync.rax.io_nautobots.yaml index 75e29cc22..b00c05de8 100644 --- a/go/nautobotop/config/crd/bases/sync.rax.io_nautobots.yaml +++ b/go/nautobotop/config/crd/bases/sync.rax.io_nautobots.yaml @@ -270,6 +270,31 @@ spec: pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ type: string type: object + prefixRef: + items: + description: ConfigMapRef defines a reference to a specific ConfigMap + properties: + configMapSelector: + description: The name of the ConfigMap resource being referred + to + properties: + key: + description: The key in the ConfigMap data + type: string + name: + description: The name of the ConfigMap + minLength: 1 + type: string + namespace: + description: The namespace where the ConfigMap resides + type: string + required: + - name + type: object + required: + - configMapSelector + type: object + type: array rackGroupRef: items: description: ConfigMapRef defines a reference to a specific ConfigMap @@ -323,9 +348,109 @@ spec: requeueAfter: default: 600 type: integer + rirRef: + items: + description: ConfigMapRef defines a reference to a specific ConfigMap + properties: + configMapSelector: + description: The name of the ConfigMap resource being referred + to + properties: + key: + description: The key in the ConfigMap data + type: string + name: + description: The name of the ConfigMap + minLength: 1 + type: string + namespace: + description: The namespace where the ConfigMap resides + type: string + required: + - name + type: object + required: + - configMapSelector + type: object + type: array + roleRef: + items: + description: ConfigMapRef defines a reference to a specific ConfigMap + properties: + configMapSelector: + description: The name of the ConfigMap resource being referred + to + properties: + key: + description: The key in the ConfigMap data + type: string + name: + description: The name of the ConfigMap + minLength: 1 + type: string + namespace: + description: The namespace where the ConfigMap resides + type: string + required: + - name + type: object + required: + - configMapSelector + type: object + type: array syncIntervalSeconds: default: 172800 type: integer + tenantGroupRef: + items: + description: ConfigMapRef defines a reference to a specific ConfigMap + properties: + configMapSelector: + description: The name of the ConfigMap resource being referred + to + properties: + key: + description: The key in the ConfigMap data + type: string + name: + description: The name of the ConfigMap + minLength: 1 + type: string + namespace: + description: The namespace where the ConfigMap resides + type: string + required: + - name + type: object + required: + - configMapSelector + type: object + type: array + tenantRef: + items: + description: ConfigMapRef defines a reference to a specific ConfigMap + properties: + configMapSelector: + description: The name of the ConfigMap resource being referred + to + properties: + key: + description: The key in the ConfigMap data + type: string + name: + description: The name of the ConfigMap + minLength: 1 + type: string + namespace: + description: The namespace where the ConfigMap resides + type: string + required: + - name + type: object + required: + - configMapSelector + type: object + type: array vlanGroupRef: items: description: ConfigMapRef defines a reference to a specific ConfigMap @@ -351,6 +476,31 @@ spec: - configMapSelector type: object type: array + vlanRef: + items: + description: ConfigMapRef defines a reference to a specific ConfigMap + properties: + configMapSelector: + description: The name of the ConfigMap resource being referred + to + properties: + key: + description: The key in the ConfigMap data + type: string + name: + description: The name of the ConfigMap + minLength: 1 + type: string + namespace: + description: The namespace where the ConfigMap resides + type: string + required: + - name + type: object + required: + - configMapSelector + type: object + type: array type: object status: description: NautobotStatus defines the observed state of Nautobot. diff --git a/go/nautobotop/go.mod b/go/nautobotop/go.mod index d88bfba80..21632d42b 100644 --- a/go/nautobotop/go.mod +++ b/go/nautobotop/go.mod @@ -4,6 +4,7 @@ go 1.24.0 require ( github.com/charmbracelet/log v0.4.2 + github.com/imroc/req/v3 v3.57.0 github.com/maypok86/otter/v2 v2.3.0 github.com/nautobot/go-nautobot/v3 v3.0.0-20241004125816-802218866be0 github.com/onsi/ginkgo/v2 v2.22.0 @@ -18,6 +19,7 @@ require ( require ( cel.dev/expr v0.19.1 // indirect + github.com/andybalholm/brotli v1.2.0 // indirect github.com/antlr4-go/antlr/v4 v4.13.0 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect @@ -48,12 +50,15 @@ require ( github.com/google/cel-go v0.23.2 // indirect github.com/google/gnostic-models v0.6.9 // indirect github.com/google/go-cmp v0.7.0 // indirect + github.com/google/go-querystring v1.1.0 // indirect github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db // indirect github.com/google/uuid v1.6.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 // indirect + github.com/icholy/digest v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.18.2 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-isatty v0.0.20 // indirect @@ -68,6 +73,9 @@ require ( github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.62.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect + github.com/quic-go/qpack v0.6.0 // indirect + github.com/quic-go/quic-go v0.57.1 // indirect + github.com/refraction-networking/utls v1.8.1 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/spf13/cobra v1.8.1 // indirect github.com/spf13/pflag v1.0.5 // indirect @@ -86,15 +94,16 @@ require ( go.opentelemetry.io/proto/otlp v1.4.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect + golang.org/x/crypto v0.46.0 // indirect golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect - golang.org/x/net v0.38.0 // indirect + golang.org/x/net v0.48.0 // indirect golang.org/x/oauth2 v0.27.0 // indirect - golang.org/x/sync v0.12.0 // indirect - golang.org/x/sys v0.31.0 // indirect - golang.org/x/term v0.30.0 // indirect - golang.org/x/text v0.23.0 // indirect - golang.org/x/time v0.9.0 // indirect - golang.org/x/tools v0.26.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.39.0 // indirect + golang.org/x/term v0.38.0 // indirect + golang.org/x/text v0.32.0 // indirect + golang.org/x/time v0.12.0 // indirect + golang.org/x/tools v0.39.0 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 // indirect diff --git a/go/nautobotop/go.sum b/go/nautobotop/go.sum index b7112dbfb..849d2a492 100644 --- a/go/nautobotop/go.sum +++ b/go/nautobotop/go.sum @@ -2,6 +2,8 @@ cel.dev/expr v0.19.1 h1:NciYrtDRIR0lNCnH1LFJegdjspNx9fI59O7TWcua/W4= cel.dev/expr v0.19.1/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= github.com/abhimanyu003/go-nautobot/v3 v3.0.1 h1:ajG1GbYKSa91Mk+3EP6ucsN4+jctAE/cYCIQgqKl4tY= github.com/abhimanyu003/go-nautobot/v3 v3.0.1/go.mod h1:syPUSxlp3KXMTLetxOBvWPz2+JHfQFaQUxYWmvw9Ljo= +github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= +github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= @@ -72,9 +74,12 @@ github.com/google/cel-go v0.23.2 h1:UdEe3CvQh3Nv+E/j9r1Y//WO0K0cSyD7/y0bzyLIMI4= github.com/google/cel-go v0.23.2/go.mod h1:52Pb6QsDbC5kvgxvZhiL9QX1oZEkcUF/ZqaPx1J5Wwo= github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -84,6 +89,10 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 h1:TmHmbvxPmaegwhDubVz0lICL0J5Ka2vwTzhoePEXsGE= github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0/go.mod h1:qztMSjm835F2bXf+5HKAPIS5qsmQDqZna/PgVt4rWtI= +github.com/icholy/digest v1.1.0 h1:HfGg9Irj7i+IX1o1QAmPfIBNu/Q5A5Tu3n/MED9k9H4= +github.com/icholy/digest v1.1.0/go.mod h1:QNrsSGQ5v7v9cReDI0+eyjsXGUoRSUZQHeQ5C4XLa0Y= +github.com/imroc/req/v3 v3.57.0 h1:LMTUjNRUybUkTPn8oJDq8Kg3JRBOBTcnDhKu7mzupKI= +github.com/imroc/req/v3 v3.57.0/go.mod h1:JL62ey1nvSLq81HORNcosvlf7SxZStONNqOprg0Pz00= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= @@ -92,8 +101,8 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= -github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= +github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -138,6 +147,12 @@ github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= +github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= +github.com/quic-go/quic-go v0.57.1 h1:25KAAR9QR8KZrCZRThWMKVAwGoiHIrNbT72ULHTuI10= +github.com/quic-go/quic-go v0.57.1/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s= +github.com/refraction-networking/utls v1.8.1 h1:yNY1kapmQU8JeM1sSw2H2asfTIwWxIkrMJI0pRUOCAo= +github.com/refraction-networking/utls v1.8.1/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= @@ -167,6 +182,8 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= +github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= @@ -189,6 +206,8 @@ go.opentelemetry.io/proto/otlp v1.4.0 h1:TA9WRvW6zMwP+Ssb6fLoUIuirti1gGbP28GcKG1 go.opentelemetry.io/proto/otlp v1.4.0/go.mod h1:PPBWZIP98o2ElSqI35IHfu7hIhSwvc5N38Jw8pXuGFY= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= +go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= @@ -198,6 +217,8 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -206,35 +227,35 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= -golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= -golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= -golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= -golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= +golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= -golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= -golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= +golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= -golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= +golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= +golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -261,6 +282,8 @@ gopkg.in/validator.v2 v2.0.1/go.mod h1:lIUZBlB3Im4s/eYp39Ry/wkR02yOPhZ9IwIRBjuPu gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= +gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= k8s.io/api v0.33.0 h1:yTgZVn1XEe6opVpP1FylmNrIFWuDqe2H0V8CT5gxfIU= k8s.io/api v0.33.0/go.mod h1:CTO61ECK/KU7haa3qq8sarQ0biLq2ju405IZAd9zsiM= k8s.io/apiextensions-apiserver v0.33.0 h1:d2qpYL7Mngbsc1taA4IjJPRJ9ilnsXIrndH+r9IimOs= diff --git a/go/nautobotop/helm/crds/clients.yaml b/go/nautobotop/helm/crds/clients.yaml index 07572b3e1..b00c05de8 100644 --- a/go/nautobotop/helm/crds/clients.yaml +++ b/go/nautobotop/helm/crds/clients.yaml @@ -195,6 +195,31 @@ spec: - configMapSelector type: object type: array + namespaceRef: + items: + description: ConfigMapRef defines a reference to a specific ConfigMap + properties: + configMapSelector: + description: The name of the ConfigMap resource being referred + to + properties: + key: + description: The key in the ConfigMap data + type: string + name: + description: The name of the ConfigMap + minLength: 1 + type: string + namespace: + description: The namespace where the ConfigMap resides + type: string + required: + - name + type: object + required: + - configMapSelector + type: object + type: array nautobotSecretRef: properties: name: @@ -245,6 +270,31 @@ spec: pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ type: string type: object + prefixRef: + items: + description: ConfigMapRef defines a reference to a specific ConfigMap + properties: + configMapSelector: + description: The name of the ConfigMap resource being referred + to + properties: + key: + description: The key in the ConfigMap data + type: string + name: + description: The name of the ConfigMap + minLength: 1 + type: string + namespace: + description: The namespace where the ConfigMap resides + type: string + required: + - name + type: object + required: + - configMapSelector + type: object + type: array rackGroupRef: items: description: ConfigMapRef defines a reference to a specific ConfigMap @@ -298,9 +348,109 @@ spec: requeueAfter: default: 600 type: integer + rirRef: + items: + description: ConfigMapRef defines a reference to a specific ConfigMap + properties: + configMapSelector: + description: The name of the ConfigMap resource being referred + to + properties: + key: + description: The key in the ConfigMap data + type: string + name: + description: The name of the ConfigMap + minLength: 1 + type: string + namespace: + description: The namespace where the ConfigMap resides + type: string + required: + - name + type: object + required: + - configMapSelector + type: object + type: array + roleRef: + items: + description: ConfigMapRef defines a reference to a specific ConfigMap + properties: + configMapSelector: + description: The name of the ConfigMap resource being referred + to + properties: + key: + description: The key in the ConfigMap data + type: string + name: + description: The name of the ConfigMap + minLength: 1 + type: string + namespace: + description: The namespace where the ConfigMap resides + type: string + required: + - name + type: object + required: + - configMapSelector + type: object + type: array syncIntervalSeconds: default: 172800 type: integer + tenantGroupRef: + items: + description: ConfigMapRef defines a reference to a specific ConfigMap + properties: + configMapSelector: + description: The name of the ConfigMap resource being referred + to + properties: + key: + description: The key in the ConfigMap data + type: string + name: + description: The name of the ConfigMap + minLength: 1 + type: string + namespace: + description: The namespace where the ConfigMap resides + type: string + required: + - name + type: object + required: + - configMapSelector + type: object + type: array + tenantRef: + items: + description: ConfigMapRef defines a reference to a specific ConfigMap + properties: + configMapSelector: + description: The name of the ConfigMap resource being referred + to + properties: + key: + description: The key in the ConfigMap data + type: string + name: + description: The name of the ConfigMap + minLength: 1 + type: string + namespace: + description: The namespace where the ConfigMap resides + type: string + required: + - name + type: object + required: + - configMapSelector + type: object + type: array vlanGroupRef: items: description: ConfigMapRef defines a reference to a specific ConfigMap @@ -326,6 +476,31 @@ spec: - configMapSelector type: object type: array + vlanRef: + items: + description: ConfigMapRef defines a reference to a specific ConfigMap + properties: + configMapSelector: + description: The name of the ConfigMap resource being referred + to + properties: + key: + description: The key in the ConfigMap data + type: string + name: + description: The name of the ConfigMap + minLength: 1 + type: string + namespace: + description: The namespace where the ConfigMap resides + type: string + required: + - name + type: object + required: + - configMapSelector + type: object + type: array type: object status: description: NautobotStatus defines the observed state of Nautobot. diff --git a/go/nautobotop/internal/controller/nautobot_controller.go b/go/nautobotop/internal/controller/nautobot_controller.go index 20cb2eb10..1685682f2 100644 --- a/go/nautobotop/internal/controller/nautobot_controller.go +++ b/go/nautobotop/internal/controller/nautobot_controller.go @@ -86,16 +86,29 @@ func (r *NautobotReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c // Define all resources to sync resources := []resourceConfig{ + // No Dependencies, these need to be run first and are independent. {name: "locationTypes", configRefs: nautobotCR.Spec.LocationTypesRef, syncFunc: r.syncLocationTypes}, {name: "location", configRefs: nautobotCR.Spec.LocationRef, syncFunc: r.syncLocation}, - {name: "rackGroup", configRefs: nautobotCR.Spec.RackGroupRef, syncFunc: r.syncRackGroup}, - {name: "rack", configRefs: nautobotCR.Spec.RackRef, syncFunc: r.syncRack}, + {name: "rir", configRefs: nautobotCR.Spec.RirRef, syncFunc: r.syncRir}, + {name: "role", configRefs: nautobotCR.Spec.RoleRef, syncFunc: r.syncRole}, {name: "deviceType", configRefs: nautobotCR.Spec.DeviceTypesRef, syncFunc: r.syncDeviceTypes}, - {name: "vlanGroup", configRefs: nautobotCR.Spec.VlanGroupRef, syncFunc: r.syncVlanGroup}, {name: "clusterType", configRefs: nautobotCR.Spec.ClusterTypeRef, syncFunc: r.syncClusterType}, {name: "clusterGroup", configRefs: nautobotCR.Spec.ClusterGroupRef, syncFunc: r.syncClusterGroup}, {name: "cluster", configRefs: nautobotCR.Spec.ClusterRef, syncFunc: r.syncCluster}, + // depends on: location + {name: "rackGroup", configRefs: nautobotCR.Spec.RackGroupRef, syncFunc: r.syncRackGroup}, + {name: "vlanGroup", configRefs: nautobotCR.Spec.VlanGroupRef, syncFunc: r.syncVlanGroup}, + // depends on: location, rackGroup + {name: "rack", configRefs: nautobotCR.Spec.RackRef, syncFunc: r.syncRack}, + // tenancy tenantGroup before tenant + {name: "tenantGroup", configRefs: nautobotCR.Spec.TenantGroupRef, syncFunc: r.syncTenantGroup}, + {name: "tenant", configRefs: nautobotCR.Spec.TenantRef, syncFunc: r.syncTenant}, + // depends on: location, tenant, tenantGroup {name: "namespace", configRefs: nautobotCR.Spec.NamespaceRef, syncFunc: r.syncNamespace}, + // depends on: vlanGroup, location, tenant, tenantGroup, role + {name: "vlan", configRefs: nautobotCR.Spec.VlanRef, syncFunc: r.syncVlan}, + // depends on: namespace, rir, location, vlanGroup, vlan, tenant, tenantGroup, role + {name: "prefix", configRefs: nautobotCR.Spec.PrefixRef, syncFunc: r.syncPrefix}, } // Aggregate data and check sync decisions for all resources @@ -298,6 +311,38 @@ func (r *NautobotReconciler) syncVlanGroup(ctx context.Context, return nil } +func (r *NautobotReconciler) syncVlan(ctx context.Context, + nautobotClient *nbClient.NautobotClient, + vlanData map[string]string, +) error { + log := logf.FromContext(ctx) + log.Info("syncing vlans", "count", len(vlanData)) + if len(vlanData) == 0 { + return nil + } + syncSvc := sync.NewVlanSync(nautobotClient) + if err := syncSvc.SyncAll(ctx, vlanData); err != nil { + return fmt.Errorf("failed to sync vlans: %w", err) + } + return nil +} + +func (r *NautobotReconciler) syncPrefix(ctx context.Context, + nautobotClient *nbClient.NautobotClient, + prefixData map[string]string, +) error { + log := logf.FromContext(ctx) + log.Info("syncing prefixes", "count", len(prefixData)) + if len(prefixData) == 0 { + return nil + } + syncSvc := sync.NewPrefixSync(nautobotClient) + if err := syncSvc.SyncAll(ctx, prefixData); err != nil { + return fmt.Errorf("failed to sync prefixes: %w", err) + } + return nil +} + func (r *NautobotReconciler) syncClusterType(ctx context.Context, nautobotClient *nbClient.NautobotClient, clusterTypeData map[string]string, @@ -361,6 +406,70 @@ func (r *NautobotReconciler) syncNamespace(ctx context.Context, return nil } +func (r *NautobotReconciler) syncRir(ctx context.Context, + nautobotClient *nbClient.NautobotClient, + rirData map[string]string, +) error { + log := logf.FromContext(ctx) + log.Info("syncing rirs", "count", len(rirData)) + if len(rirData) == 0 { + return nil + } + syncSvc := sync.NewRirSync(nautobotClient) + if err := syncSvc.SyncAll(ctx, rirData); err != nil { + return fmt.Errorf("failed to sync rirs: %w", err) + } + return nil +} + +func (r *NautobotReconciler) syncRole(ctx context.Context, + nautobotClient *nbClient.NautobotClient, + roleData map[string]string, +) error { + log := logf.FromContext(ctx) + log.Info("syncing roles", "count", len(roleData)) + if len(roleData) == 0 { + return nil + } + syncSvc := sync.NewRoleSync(nautobotClient) + if err := syncSvc.SyncAll(ctx, roleData); err != nil { + return fmt.Errorf("failed to sync roles: %w", err) + } + return nil +} + +func (r *NautobotReconciler) syncTenantGroup(ctx context.Context, + nautobotClient *nbClient.NautobotClient, + tenantGroupData map[string]string, +) error { + log := logf.FromContext(ctx) + log.Info("syncing tenant groups", "count", len(tenantGroupData)) + if len(tenantGroupData) == 0 { + return nil + } + syncSvc := sync.NewTenantGroupSync(nautobotClient) + if err := syncSvc.SyncAll(ctx, tenantGroupData); err != nil { + return fmt.Errorf("failed to sync tenant groups: %w", err) + } + return nil +} + +func (r *NautobotReconciler) syncTenant(ctx context.Context, + nautobotClient *nbClient.NautobotClient, + tenantData map[string]string, +) error { + log := logf.FromContext(ctx) + log.Info("syncing tenants", "count", len(tenantData)) + if len(tenantData) == 0 { + return nil + } + syncSvc := sync.NewTenantSync(nautobotClient) + if err := syncSvc.SyncAll(ctx, tenantData); err != nil { + return fmt.Errorf("failed to sync tenants: %w", err) + } + return nil +} + // getAuthTokenFromSecretRef: this will fetch Nautobot auth token from the given refer. func (r *NautobotReconciler) getAuthTokenFromSecretRef(ctx context.Context, nautobotCR syncv1alpha1.Nautobot) (string, string, error) { var username, token string diff --git a/go/nautobotop/internal/nautobot/client/cache.go b/go/nautobotop/internal/nautobot/client/cache.go index 91623c6c4..b401b4526 100644 --- a/go/nautobotop/internal/nautobot/client/cache.go +++ b/go/nautobotop/internal/nautobot/client/cache.go @@ -48,6 +48,36 @@ func (n *NautobotClient) PreLoadCacheForLookup(ctx context.Context) error { log.Info("pre-load vlan groups cache", "count", len(list.Results)) } + if list, _, err := n.APIClient.IpamAPI.IpamVlansList(ctx).Depth(2).Execute(); err == nil && list != nil { + n.Cache.SetCollection("vlans", list.Results) + log.Info("pre-load vlans cache", "count", len(list.Results)) + } + + if list, _, err := n.APIClient.IpamAPI.IpamPrefixesList(ctx).Depth(2).Execute(); err == nil && list != nil { + n.Cache.SetCollection("prefixes", list.Results) + log.Info("pre-load prefixes cache", "count", len(list.Results)) + } + + if list, _, err := n.APIClient.IpamAPI.IpamRirsList(ctx).Depth(2).Execute(); err == nil && list != nil { + n.Cache.SetCollection("rirs", list.Results) + log.Info("pre-load rirs cache", "count", len(list.Results)) + } + + if list, _, err := n.APIClient.IpamAPI.IpamVrfsList(ctx).Depth(2).Execute(); err == nil && list != nil { + n.Cache.SetCollection("vrfs", list.Results) + log.Info("pre-load vrfs cache", "count", len(list.Results)) + } + + if list, _, err := n.APIClient.ExtrasAPI.ExtrasRolesList(ctx).Depth(2).Execute(); err == nil && list != nil { + n.Cache.SetCollection("roles", list.Results) + log.Info("pre-load roles cache", "count", len(list.Results)) + } + + if list, _, err := n.APIClient.ExtrasAPI.ExtrasTagsList(ctx).Depth(2).Execute(); err == nil && list != nil { + n.Cache.SetCollection("tags", list.Results) + log.Info("pre-load tags cache", "count", len(list.Results)) + } + if list, _, err := n.APIClient.VirtualizationAPI.VirtualizationClusterTypesList(ctx).Depth(2).Execute(); err == nil && list != nil { n.Cache.SetCollection("clustertypes", list.Results) log.Info("pre-load cluster types cache", "count", len(list.Results)) diff --git a/go/nautobotop/internal/nautobot/client/client.go b/go/nautobotop/internal/nautobot/client/client.go index 5a1dcaa6f..e215386fb 100644 --- a/go/nautobotop/internal/nautobot/client/client.go +++ b/go/nautobotop/internal/nautobot/client/client.go @@ -4,8 +4,10 @@ import ( "context" "fmt" "strings" + "time" "github.com/charmbracelet/log" + "github.com/imroc/req/v3" nb "github.com/nautobot/go-nautobot/v3" "github.com/rackerlabs/understack/go/nautobotop/internal/nautobot/cache" "github.com/samber/lo" @@ -27,26 +29,29 @@ func (n *NautobotClient) AddReport(key string, line ...string) { log.Error(key, combined) } -// NewNautobotClient creates and configures a new Nautobot API client. -// apiURL: The base URL of the Nautobot API (e.g., "http://localhost:8000"). -// authToken: The API token for authentication. // NewNautobotClient creates and configures a new Nautobot API client. // apiURL: The base URL of the Nautobot API (e.g., "http://localhost:8000"). // authToken: The API token for authentication. // cacheMaxSize: The maximum size of the cache (0 uses default of 70,000). func NewNautobotClient(apiURL string, username, authToken string, cacheMaxSize int) (*NautobotClient, error) { + // Configure req client with retry and backoff + reqClient := req.C(). + SetTimeout(30*time.Second). + SetCommonRetryCount(3). + SetCommonRetryBackoffInterval(1*time.Second, 5*time.Second). + SetCommonRetryCondition(func(resp *req.Response, err error) bool { + return err != nil || resp.StatusCode >= 500 + }) + config := nb.NewConfiguration() + config.HTTPClient = reqClient.GetClient() config.Servers = nb.ServerConfigurations{ { URL: apiURL, }, } - // Add Authorization token header - if authToken != "" { - config.AddDefaultHeader("Authorization", fmt.Sprintf("Token %s", authToken)) - } + config.AddDefaultHeader("Authorization", fmt.Sprintf("Token %s", authToken)) client := nb.NewAPIClient(config) - c, err := cache.New(cacheMaxSize) if err != nil { return nil, fmt.Errorf("failed to create cache: %w", err) diff --git a/go/nautobotop/internal/nautobot/extras/role.go b/go/nautobotop/internal/nautobot/extras/role.go new file mode 100644 index 000000000..4b37d5a36 --- /dev/null +++ b/go/nautobotop/internal/nautobot/extras/role.go @@ -0,0 +1,101 @@ +package extras + +import ( + "context" + + "github.com/rackerlabs/understack/go/nautobotop/internal/nautobot/cache" + "github.com/rackerlabs/understack/go/nautobotop/internal/nautobot/client" + + "github.com/charmbracelet/log" + nb "github.com/nautobot/go-nautobot/v3" + "github.com/rackerlabs/understack/go/nautobotop/internal/nautobot/helpers" +) + +type RoleService struct { + client *client.NautobotClient +} + +func NewRoleService(nautobotClient *client.NautobotClient) *RoleService { + return &RoleService{ + client: nautobotClient, + } +} + +func (s *RoleService) Create(ctx context.Context, req nb.RoleRequest) (*nb.Role, error) { + role, resp, err := s.client.APIClient.ExtrasAPI.ExtrasRolesCreate(ctx).RoleRequest(req).Execute() + if err != nil { + bodyString := helpers.ReadResponseBody(resp) + s.client.AddReport("createNewRole", "failed to create", "model", req.Name, "error", err.Error(), "response_body", bodyString) + return nil, err + } + log.Info("CreateRole", "created role", role.Name) + cache.AddToCollection(s.client.Cache, "roles", *role) + return role, nil +} + +func (s *RoleService) GetByName(ctx context.Context, name string) nb.Role { + if role, ok := cache.FindByName(s.client.Cache, "roles", name, func(r nb.Role) string { + return r.Name + }); ok { + return role + } + + list, resp, err := s.client.APIClient.ExtrasAPI.ExtrasRolesList(ctx).Depth(2).Name([]string{name}).Execute() + if err != nil { + bodyString := helpers.ReadResponseBody(resp) + s.client.AddReport("GetRoleByName", "failed to get", "name", name, "error", err.Error(), "response_body", bodyString) + return nb.Role{} + } + if list == nil || len(list.Results) == 0 { + return nb.Role{} + } + if list.Results[0].Id == nil { + return nb.Role{} + } + + return list.Results[0] +} + +func (s *RoleService) ListAll(ctx context.Context) []nb.Role { + ids := s.client.GetChangeObjectIDS(ctx, "extras.role") + list, resp, err := s.client.APIClient.ExtrasAPI.ExtrasRolesList(ctx).Id(ids).Depth(2).Execute() + if err != nil { + bodyString := helpers.ReadResponseBody(resp) + s.client.AddReport("ListAllRoles", "failed to list", "error", err.Error(), "response_body", bodyString) + return []nb.Role{} + } + if list == nil || len(list.Results) == 0 { + return []nb.Role{} + } + if list.Results[0].Id == nil { + return []nb.Role{} + } + return list.Results +} + +func (s *RoleService) Update(ctx context.Context, id string, req nb.RoleRequest) (*nb.Role, error) { + role, resp, err := s.client.APIClient.ExtrasAPI.ExtrasRolesUpdate(ctx, id).RoleRequest(req).Execute() + if err != nil { + bodyString := helpers.ReadResponseBody(resp) + s.client.AddReport("UpdateRole", "failed to update", "id", id, "model", req.Name, "error", err.Error(), "response_body", bodyString) + return nil, err + } + log.Info("successfully updated role", "id", id, "model", role.GetName()) + cache.UpdateInCollection(s.client.Cache, "roles", *role, func(r nb.Role) bool { + return r.Id != nil && *r.Id == id + }) + return role, nil +} + +func (s *RoleService) Destroy(ctx context.Context, id string) error { + resp, err := s.client.APIClient.ExtrasAPI.ExtrasRolesDestroy(ctx, id).Execute() + if err != nil { + bodyString := helpers.ReadResponseBody(resp) + s.client.AddReport("DestroyRole", "failed to destroy", "id", id, "error", err.Error(), "response_body", bodyString) + return err + } + cache.RemoveFromCollection(s.client.Cache, "roles", func(r nb.Role) bool { + return r.Id != nil && *r.Id == id + }) + return nil +} diff --git a/go/nautobotop/internal/nautobot/extras/tag.go b/go/nautobotop/internal/nautobot/extras/tag.go new file mode 100644 index 000000000..ceb1c13eb --- /dev/null +++ b/go/nautobotop/internal/nautobot/extras/tag.go @@ -0,0 +1,44 @@ +package extras + +import ( + "context" + + "github.com/rackerlabs/understack/go/nautobotop/internal/nautobot/cache" + "github.com/rackerlabs/understack/go/nautobotop/internal/nautobot/client" + + nb "github.com/nautobot/go-nautobot/v3" + "github.com/rackerlabs/understack/go/nautobotop/internal/nautobot/helpers" +) + +type TagService struct { + client *client.NautobotClient +} + +func NewTagService(nautobotClient *client.NautobotClient) *TagService { + return &TagService{ + client: nautobotClient, + } +} + +func (s *TagService) GetByName(ctx context.Context, name string) nb.Tag { + if tag, ok := cache.FindByName(s.client.Cache, "tags", name, func(t nb.Tag) string { + return t.Name + }); ok { + return tag + } + + list, resp, err := s.client.APIClient.ExtrasAPI.ExtrasTagsList(ctx).Depth(2).Name([]string{name}).Execute() + if err != nil { + bodyString := helpers.ReadResponseBody(resp) + s.client.AddReport("GetTagByName", "failed to get", "name", name, "error", err.Error(), "response_body", bodyString) + return nb.Tag{} + } + if list == nil || len(list.Results) == 0 { + return nb.Tag{} + } + if list.Results[0].Id == nil { + return nb.Tag{} + } + + return list.Results[0] +} diff --git a/go/nautobotop/internal/nautobot/helpers/helpers.go b/go/nautobotop/internal/nautobot/helpers/helpers.go index 33bab2101..caeb4438d 100644 --- a/go/nautobotop/internal/nautobot/helpers/helpers.go +++ b/go/nautobotop/internal/nautobot/helpers/helpers.go @@ -35,6 +35,24 @@ func BuildNullableBulkWritableRackRequestRackGroup(id string) nb.NullableBulkWri return *nb.NewNullableBulkWritableRackRequestRackGroup(&rackGroup) } +func BuildNullableBulkWritablePrefixRequestLocation(id string) nb.NullableBulkWritablePrefixRequestLocation { + location := nb.BulkWritablePrefixRequestLocation{ + Id: &nb.ApprovalWorkflowApprovalWorkflowDefinitionId{ + String: &id, + }, + } + return *nb.NewNullableBulkWritablePrefixRequestLocation(&location) +} + +func BuildNullableBulkWritablePrefixRequestRir(id string) nb.NullableBulkWritablePrefixRequestRir { + rir := nb.BulkWritablePrefixRequestRir{ + Id: &nb.ApprovalWorkflowApprovalWorkflowDefinitionId{ + String: &id, + }, + } + return *nb.NewNullableBulkWritablePrefixRequestRir(&rir) +} + // ReadResponseBody safely reads and closes the response body. // Returns the body content as a string. If resp is nil, returns empty string. func ReadResponseBody(resp *http.Response) string { diff --git a/go/nautobotop/internal/nautobot/ipam/prefix.go b/go/nautobotop/internal/nautobot/ipam/prefix.go new file mode 100644 index 000000000..446dc087b --- /dev/null +++ b/go/nautobotop/internal/nautobot/ipam/prefix.go @@ -0,0 +1,106 @@ +package ipam + +import ( + "context" + + "github.com/rackerlabs/understack/go/nautobotop/internal/nautobot/cache" + "github.com/rackerlabs/understack/go/nautobotop/internal/nautobot/client" + + "github.com/charmbracelet/log" + nb "github.com/nautobot/go-nautobot/v3" + "github.com/rackerlabs/understack/go/nautobotop/internal/nautobot/helpers" +) + +type PrefixService struct { + client *client.NautobotClient +} + +func NewPrefixService(nautobotClient *client.NautobotClient) *PrefixService { + return &PrefixService{ + client: nautobotClient, + } +} + +func (s *PrefixService) Create(ctx context.Context, req nb.WritablePrefixRequest) (*nb.Prefix, error) { + prefix, resp, err := s.client.APIClient.IpamAPI.IpamPrefixesCreate(ctx).WritablePrefixRequest(req).Execute() + if err != nil { + bodyString := helpers.ReadResponseBody(resp) + s.client.AddReport("createNewPrefix", "failed to create", "model", req.Prefix, "error", err.Error(), "response_body", bodyString) + return nil, err + } + log.Info("CreatePrefix", "created prefix", prefix.Prefix) + cache.AddToCollection(s.client.Cache, "prefixes", *prefix) + + return prefix, nil +} + +func (s *PrefixService) GetByPrefix(ctx context.Context, prefix string) nb.Prefix { + if p, ok := cache.FindByName(s.client.Cache, "prefixes", prefix, func(p nb.Prefix) string { + return p.Prefix + }); ok { + return p + } + + list, resp, err := s.client.APIClient.IpamAPI.IpamPrefixesList(ctx).Depth(2).Prefix([]string{prefix}).Execute() + if err != nil { + bodyString := helpers.ReadResponseBody(resp) + s.client.AddReport("GetPrefixByPrefix", "failed to get", "prefix", prefix, "error", err.Error(), "response_body", bodyString) + return nb.Prefix{} + } + if list == nil || len(list.Results) == 0 { + return nb.Prefix{} + } + if list.Results[0].Id == nil { + return nb.Prefix{} + } + + return list.Results[0] +} + +func (s *PrefixService) ListAll(ctx context.Context) []nb.Prefix { + ids := s.client.GetChangeObjectIDS(ctx, "ipam.prefix") + list, resp, err := s.client.APIClient.IpamAPI.IpamPrefixesList(ctx).Id(ids).Depth(2).Execute() + if err != nil { + bodyString := helpers.ReadResponseBody(resp) + s.client.AddReport("ListAllPrefixes", "failed to list", "error", err.Error(), "response_body", bodyString) + return []nb.Prefix{} + } + if list == nil || len(list.Results) == 0 { + return []nb.Prefix{} + } + if list.Results[0].Id == nil { + return []nb.Prefix{} + } + + return list.Results +} + +func (s *PrefixService) Update(ctx context.Context, id string, req nb.WritablePrefixRequest) (*nb.Prefix, error) { + prefix, resp, err := s.client.APIClient.IpamAPI.IpamPrefixesUpdate(ctx, id).WritablePrefixRequest(req).Execute() + if err != nil { + bodyString := helpers.ReadResponseBody(resp) + s.client.AddReport("UpdatePrefix", "failed to update", "id", id, "model", req.Prefix, "error", err.Error(), "response_body", bodyString) + return nil, err + } + log.Info("successfully updated prefix", "id", id, "model", prefix.GetPrefix()) + + cache.UpdateInCollection(s.client.Cache, "prefixes", *prefix, func(p nb.Prefix) bool { + return p.Id != nil && *p.Id == id + }) + + return prefix, nil +} + +func (s *PrefixService) Destroy(ctx context.Context, id string) error { + resp, err := s.client.APIClient.IpamAPI.IpamPrefixesDestroy(ctx, id).Execute() + if err != nil { + bodyString := helpers.ReadResponseBody(resp) + s.client.AddReport("DestroyPrefix", "failed to destroy", "id", id, "error", err.Error(), "response_body", bodyString) + return err + } + cache.RemoveFromCollection(s.client.Cache, "prefixes", func(p nb.Prefix) bool { + return p.Id != nil && *p.Id == id + }) + + return nil +} diff --git a/go/nautobotop/internal/nautobot/ipam/rir.go b/go/nautobotop/internal/nautobot/ipam/rir.go new file mode 100644 index 000000000..492e4a18a --- /dev/null +++ b/go/nautobotop/internal/nautobot/ipam/rir.go @@ -0,0 +1,101 @@ +package ipam + +import ( + "context" + + "github.com/rackerlabs/understack/go/nautobotop/internal/nautobot/cache" + "github.com/rackerlabs/understack/go/nautobotop/internal/nautobot/client" + + "github.com/charmbracelet/log" + nb "github.com/nautobot/go-nautobot/v3" + "github.com/rackerlabs/understack/go/nautobotop/internal/nautobot/helpers" +) + +type RirService struct { + client *client.NautobotClient +} + +func NewRirService(nautobotClient *client.NautobotClient) *RirService { + return &RirService{ + client: nautobotClient, + } +} + +func (s *RirService) Create(ctx context.Context, req nb.RIRRequest) (*nb.RIR, error) { + rir, resp, err := s.client.APIClient.IpamAPI.IpamRirsCreate(ctx).RIRRequest(req).Execute() + if err != nil { + bodyString := helpers.ReadResponseBody(resp) + s.client.AddReport("createNewRir", "failed to create", "model", req.Name, "error", err.Error(), "response_body", bodyString) + return nil, err + } + log.Info("CreateRir", "created rir", rir.Name) + cache.AddToCollection(s.client.Cache, "rirs", *rir) + return rir, nil +} + +func (s *RirService) GetByName(ctx context.Context, name string) nb.RIR { + if rir, ok := cache.FindByName(s.client.Cache, "rirs", name, func(r nb.RIR) string { + return r.Name + }); ok { + return rir + } + + list, resp, err := s.client.APIClient.IpamAPI.IpamRirsList(ctx).Depth(2).Name([]string{name}).Execute() + if err != nil { + bodyString := helpers.ReadResponseBody(resp) + s.client.AddReport("GetRirByName", "failed to get", "name", name, "error", err.Error(), "response_body", bodyString) + return nb.RIR{} + } + if list == nil || len(list.Results) == 0 { + return nb.RIR{} + } + if list.Results[0].Id == nil { + return nb.RIR{} + } + + return list.Results[0] +} + +func (s *RirService) ListAll(ctx context.Context) []nb.RIR { + ids := s.client.GetChangeObjectIDS(ctx, "ipam.rir") + list, resp, err := s.client.APIClient.IpamAPI.IpamRirsList(ctx).Id(ids).Depth(2).Execute() + if err != nil { + bodyString := helpers.ReadResponseBody(resp) + s.client.AddReport("ListAllRirs", "failed to list", "error", err.Error(), "response_body", bodyString) + return []nb.RIR{} + } + if list == nil || len(list.Results) == 0 { + return []nb.RIR{} + } + if list.Results[0].Id == nil { + return []nb.RIR{} + } + return list.Results +} + +func (s *RirService) Update(ctx context.Context, id string, req nb.RIRRequest) (*nb.RIR, error) { + rir, resp, err := s.client.APIClient.IpamAPI.IpamRirsUpdate(ctx, id).RIRRequest(req).Execute() + if err != nil { + bodyString := helpers.ReadResponseBody(resp) + s.client.AddReport("UpdateRir", "failed to update", "id", id, "model", req.Name, "error", err.Error(), "response_body", bodyString) + return nil, err + } + log.Info("successfully updated rir", "id", id, "model", rir.GetName()) + cache.UpdateInCollection(s.client.Cache, "rirs", *rir, func(r nb.RIR) bool { + return r.Id != nil && *r.Id == id + }) + return rir, nil +} + +func (s *RirService) Destroy(ctx context.Context, id string) error { + resp, err := s.client.APIClient.IpamAPI.IpamRirsDestroy(ctx, id).Execute() + if err != nil { + bodyString := helpers.ReadResponseBody(resp) + s.client.AddReport("DestroyRir", "failed to destroy", "id", id, "error", err.Error(), "response_body", bodyString) + return err + } + cache.RemoveFromCollection(s.client.Cache, "rirs", func(r nb.RIR) bool { + return r.Id != nil && *r.Id == id + }) + return nil +} diff --git a/go/nautobotop/internal/nautobot/ipam/vlan.go b/go/nautobotop/internal/nautobot/ipam/vlan.go new file mode 100644 index 000000000..4fa20573c --- /dev/null +++ b/go/nautobotop/internal/nautobot/ipam/vlan.go @@ -0,0 +1,106 @@ +package ipam + +import ( + "context" + + "github.com/rackerlabs/understack/go/nautobotop/internal/nautobot/cache" + "github.com/rackerlabs/understack/go/nautobotop/internal/nautobot/client" + + "github.com/charmbracelet/log" + nb "github.com/nautobot/go-nautobot/v3" + "github.com/rackerlabs/understack/go/nautobotop/internal/nautobot/helpers" +) + +type VlanService struct { + client *client.NautobotClient +} + +func NewVlanService(nautobotClient *client.NautobotClient) *VlanService { + return &VlanService{ + client: nautobotClient, + } +} + +func (s *VlanService) Create(ctx context.Context, req nb.VLANRequest) (*nb.VLAN, error) { + vlan, resp, err := s.client.APIClient.IpamAPI.IpamVlansCreate(ctx).VLANRequest(req).Execute() + if err != nil { + bodyString := helpers.ReadResponseBody(resp) + s.client.AddReport("createNewVlan", "failed to create", "model", req.Name, "error", err.Error(), "response_body", bodyString) + return nil, err + } + log.Info("CreateVlan", "created vlan", vlan.Name) + cache.AddToCollection(s.client.Cache, "vlans", *vlan) + + return vlan, nil +} + +func (s *VlanService) GetByName(ctx context.Context, name string) nb.VLAN { + if vlan, ok := cache.FindByName(s.client.Cache, "vlans", name, func(v nb.VLAN) string { + return v.Name + }); ok { + return vlan + } + + list, resp, err := s.client.APIClient.IpamAPI.IpamVlansList(ctx).Depth(2).Name([]string{name}).Execute() + if err != nil { + bodyString := helpers.ReadResponseBody(resp) + s.client.AddReport("GetVlanByName", "failed to get", "name", name, "error", err.Error(), "response_body", bodyString) + return nb.VLAN{} + } + if list == nil || len(list.Results) == 0 { + return nb.VLAN{} + } + if list.Results[0].Id == nil { + return nb.VLAN{} + } + + return list.Results[0] +} + +func (s *VlanService) ListAll(ctx context.Context) []nb.VLAN { + ids := s.client.GetChangeObjectIDS(ctx, "ipam.vlan") + list, resp, err := s.client.APIClient.IpamAPI.IpamVlansList(ctx).Id(ids).Depth(2).Execute() + if err != nil { + bodyString := helpers.ReadResponseBody(resp) + s.client.AddReport("ListAllVlans", "failed to list", "error", err.Error(), "response_body", bodyString) + return []nb.VLAN{} + } + if list == nil || len(list.Results) == 0 { + return []nb.VLAN{} + } + if list.Results[0].Id == nil { + return []nb.VLAN{} + } + + return list.Results +} + +func (s *VlanService) Update(ctx context.Context, id string, req nb.VLANRequest) (*nb.VLAN, error) { + vlan, resp, err := s.client.APIClient.IpamAPI.IpamVlansUpdate(ctx, id).VLANRequest(req).Execute() + if err != nil { + bodyString := helpers.ReadResponseBody(resp) + s.client.AddReport("UpdateVlan", "failed to update", "id", id, "model", req.Name, "error", err.Error(), "response_body", bodyString) + return nil, err + } + log.Info("successfully updated vlan", "id", id, "model", vlan.GetName()) + + cache.UpdateInCollection(s.client.Cache, "vlans", *vlan, func(v nb.VLAN) bool { + return v.Id != nil && *v.Id == id + }) + + return vlan, nil +} + +func (s *VlanService) Destroy(ctx context.Context, id string) error { + resp, err := s.client.APIClient.IpamAPI.IpamVlansDestroy(ctx, id).Execute() + if err != nil { + bodyString := helpers.ReadResponseBody(resp) + s.client.AddReport("DestroyVlan", "failed to destroy", "id", id, "error", err.Error(), "response_body", bodyString) + return err + } + cache.RemoveFromCollection(s.client.Cache, "vlans", func(v nb.VLAN) bool { + return v.Id != nil && *v.Id == id + }) + + return nil +} diff --git a/go/nautobotop/internal/nautobot/ipam/vrf.go b/go/nautobotop/internal/nautobot/ipam/vrf.go new file mode 100644 index 000000000..c2232662e --- /dev/null +++ b/go/nautobotop/internal/nautobot/ipam/vrf.go @@ -0,0 +1,44 @@ +package ipam + +import ( + "context" + + "github.com/rackerlabs/understack/go/nautobotop/internal/nautobot/cache" + "github.com/rackerlabs/understack/go/nautobotop/internal/nautobot/client" + + nb "github.com/nautobot/go-nautobot/v3" + "github.com/rackerlabs/understack/go/nautobotop/internal/nautobot/helpers" +) + +type VrfService struct { + client *client.NautobotClient +} + +func NewVrfService(nautobotClient *client.NautobotClient) *VrfService { + return &VrfService{ + client: nautobotClient, + } +} + +func (s *VrfService) GetByName(ctx context.Context, name string) nb.VRF { + if vrf, ok := cache.FindByName(s.client.Cache, "vrfs", name, func(v nb.VRF) string { + return v.Name + }); ok { + return vrf + } + + list, resp, err := s.client.APIClient.IpamAPI.IpamVrfsList(ctx).Depth(2).Name([]string{name}).Execute() + if err != nil { + bodyString := helpers.ReadResponseBody(resp) + s.client.AddReport("GetVrfByName", "failed to get", "name", name, "error", err.Error(), "response_body", bodyString) + return nb.VRF{} + } + if list == nil || len(list.Results) == 0 { + return nb.VRF{} + } + if list.Results[0].Id == nil { + return nb.VRF{} + } + + return list.Results[0] +} diff --git a/go/nautobotop/internal/nautobot/models/prefix.go b/go/nautobotop/internal/nautobot/models/prefix.go new file mode 100644 index 000000000..98158a864 --- /dev/null +++ b/go/nautobotop/internal/nautobot/models/prefix.go @@ -0,0 +1,23 @@ +package models + +type Prefixes struct { + Prefix []Prefix +} + +type Prefix struct { + Prefix string `json:"prefix" yaml:"prefix"` + Namespace string `json:"namespace" yaml:"namespace"` + Type string `json:"type" yaml:"type"` + Status string `json:"status" yaml:"status"` + Role string `json:"role" yaml:"role"` + Rir string `json:"rir" yaml:"rir"` + DateAllocated string `json:"date_allocated" yaml:"date_allocated"` + Description string `json:"description" yaml:"description"` + Vrfs []string `json:"vrfs" yaml:"vrfs"` + Locations []string `json:"locations" yaml:"locations"` + VlanGroup string `json:"vlan_group" yaml:"vlan_group"` + Vlan string `json:"vlan" yaml:"vlan"` + TenantGroup string `json:"tenant_group" yaml:"tenant_group"` + Tenant string `json:"tenant" yaml:"tenant"` + Tags []string `json:"tags" yaml:"tags"` +} diff --git a/go/nautobotop/internal/nautobot/models/rir.go b/go/nautobotop/internal/nautobot/models/rir.go new file mode 100644 index 000000000..ea56439a9 --- /dev/null +++ b/go/nautobotop/internal/nautobot/models/rir.go @@ -0,0 +1,11 @@ +package models + +type Rirs struct { + Rir []Rir +} + +type Rir struct { + Name string `json:"name" yaml:"name"` + IsPrivate bool `json:"is_private" yaml:"is_private"` + Description string `json:"description" yaml:"description"` +} diff --git a/go/nautobotop/internal/nautobot/models/role.go b/go/nautobotop/internal/nautobot/models/role.go new file mode 100644 index 000000000..f637eaa91 --- /dev/null +++ b/go/nautobotop/internal/nautobot/models/role.go @@ -0,0 +1,13 @@ +package models + +type Roles struct { + Role []Role +} + +type Role struct { + Name string `json:"name" yaml:"name"` + Color string `json:"color" yaml:"color"` + Description string `json:"description" yaml:"description"` + Weight int `json:"weight" yaml:"weight"` + ContentTypes []string `json:"content_types" yaml:"content_types"` +} diff --git a/go/nautobotop/internal/nautobot/models/tenant.go b/go/nautobotop/internal/nautobot/models/tenant.go new file mode 100644 index 000000000..e75fd2176 --- /dev/null +++ b/go/nautobotop/internal/nautobot/models/tenant.go @@ -0,0 +1,13 @@ +package models + +type Tenants struct { + Tenant []Tenant +} + +type Tenant struct { + Name string `json:"name" yaml:"name"` + Description string `json:"description" yaml:"description"` + Comments string `json:"comments" yaml:"comments"` + TenantGroup string `json:"tenant_group" yaml:"tenant_group"` + Tags []string `json:"tags" yaml:"tags"` +} diff --git a/go/nautobotop/internal/nautobot/models/tenantGroup.go b/go/nautobotop/internal/nautobot/models/tenantGroup.go new file mode 100644 index 000000000..19e380981 --- /dev/null +++ b/go/nautobotop/internal/nautobot/models/tenantGroup.go @@ -0,0 +1,11 @@ +package models + +type TenantGroups struct { + TenantGroup []TenantGroup +} + +type TenantGroup struct { + Name string `json:"name" yaml:"name"` + Description string `json:"description" yaml:"description"` + Parent string `json:"parent" yaml:"parent"` +} diff --git a/go/nautobotop/internal/nautobot/models/vlan.go b/go/nautobotop/internal/nautobot/models/vlan.go new file mode 100644 index 000000000..f5e4e6f11 --- /dev/null +++ b/go/nautobotop/internal/nautobot/models/vlan.go @@ -0,0 +1,19 @@ +package models + +type Vlans struct { + Vlan []Vlan +} + +type Vlan struct { + Name string `json:"name" yaml:"name"` + Vid int `json:"vid" yaml:"vid"` + Status string `json:"status" yaml:"status"` + Role string `json:"role" yaml:"role"` + Description string `json:"description" yaml:"description"` + Locations []string `json:"locations" yaml:"locations"` + VlanGroup string `json:"vlan_group" yaml:"vlan_group"` + TenantGroup string `json:"tenant_group" yaml:"tenant_group"` + Tenant string `json:"tenant" yaml:"tenant"` + DynamicGroups []string `json:"dynamic_groups" yaml:"dynamic_groups"` + Tags []string `json:"tags" yaml:"tags"` +} diff --git a/go/nautobotop/internal/nautobot/sync/prefix.go b/go/nautobotop/internal/nautobot/sync/prefix.go new file mode 100644 index 000000000..c9ae606c3 --- /dev/null +++ b/go/nautobotop/internal/nautobot/sync/prefix.go @@ -0,0 +1,330 @@ +package sync + +import ( + "context" + "fmt" + "time" + + "github.com/charmbracelet/log" + nb "github.com/nautobot/go-nautobot/v3" + "github.com/rackerlabs/understack/go/nautobotop/internal/nautobot/client" + "github.com/rackerlabs/understack/go/nautobotop/internal/nautobot/dcim" + "github.com/rackerlabs/understack/go/nautobotop/internal/nautobot/extras" + "github.com/rackerlabs/understack/go/nautobotop/internal/nautobot/helpers" + "github.com/rackerlabs/understack/go/nautobotop/internal/nautobot/ipam" + "github.com/rackerlabs/understack/go/nautobotop/internal/nautobot/models" + "github.com/rackerlabs/understack/go/nautobotop/internal/nautobot/tenancy" + "github.com/samber/lo" + "go.yaml.in/yaml/v3" +) + +type PrefixSync struct { + client *client.NautobotClient + prefixSvc *ipam.PrefixService + namespaceSvc *ipam.NamespaceService + vlanGroupSvc *ipam.VlanGroupService + vlanSvc *ipam.VlanService + vrfSvc *ipam.VrfService + rirSvc *ipam.RirService + locationSvc *dcim.LocationService + statusSvc *dcim.StatusService + roleSvc *extras.RoleService + tenantSvc *tenancy.TenantService + tenantGroupSvc *tenancy.TenantGroupService + tagSvc *extras.TagService +} + +func NewPrefixSync(nautobotClient *client.NautobotClient) *PrefixSync { + return &PrefixSync{ + client: nautobotClient.GetClient(), + prefixSvc: ipam.NewPrefixService(nautobotClient), + namespaceSvc: ipam.NewNamespaceService(nautobotClient), + vlanGroupSvc: ipam.NewVlanGroupService(nautobotClient), + vlanSvc: ipam.NewVlanService(nautobotClient), + vrfSvc: ipam.NewVrfService(nautobotClient), + rirSvc: ipam.NewRirService(nautobotClient), + locationSvc: dcim.NewLocationService(nautobotClient.GetClient()), + statusSvc: dcim.NewStatusService(nautobotClient.GetClient()), + roleSvc: extras.NewRoleService(nautobotClient), + tenantSvc: tenancy.NewTenantService(nautobotClient), + tenantGroupSvc: tenancy.NewTenantGroupService(nautobotClient), + tagSvc: extras.NewTagService(nautobotClient), + } +} + +func (s *PrefixSync) SyncAll(ctx context.Context, data map[string]string) error { + var prefixes models.Prefixes + for key, f := range data { + var yml []models.Prefix + if err := yaml.Unmarshal([]byte(f), &yml); err != nil { + s.client.AddReport("yamlFailed", "file: "+key+" error: "+err.Error()) + return err + } + prefixes.Prefix = append(prefixes.Prefix, yml...) + } + + for _, prefix := range prefixes.Prefix { + if err := s.syncSinglePrefix(ctx, prefix); err != nil { + return err + } + } + s.deleteObsoletePrefixes(ctx, prefixes) + + return nil +} + +// syncSinglePrefix handles the create/update logic for a single prefix +func (s *PrefixSync) syncSinglePrefix(ctx context.Context, prefix models.Prefix) error { + existingPrefix := s.prefixSvc.GetByPrefix(ctx, prefix.Prefix) + + // Build status reference (required) + statusRef, err := s.buildStatusReference(ctx, prefix.Status) + if err != nil { + return fmt.Errorf("failed to build status reference for prefix %s: %w", prefix.Prefix, err) + } + + prefixRequest := nb.WritablePrefixRequest{ + Prefix: prefix.Prefix, + Status: statusRef, + } + + if prefix.Description != "" { + prefixRequest.Description = nb.PtrString(prefix.Description) + } + + if prefix.Type != "" { + typeChoice := nb.PrefixTypeChoices(prefix.Type) + prefixRequest.Type = &typeChoice + } + + if prefix.Namespace != "" { + nsRef, err := s.buildNamespaceReference(ctx, prefix.Namespace) + if err != nil { + return fmt.Errorf("failed to build namespace reference for prefix %s: %w", prefix.Prefix, err) + } + prefixRequest.Namespace = nsRef + } + + if prefix.Role != "" { + roleRef, err := s.buildRoleReference(ctx, prefix.Role) + if err != nil { + return fmt.Errorf("failed to build role reference for prefix %s: %w", prefix.Prefix, err) + } + prefixRequest.Role = roleRef + } + + if prefix.Rir != "" { + rirRef, err := s.buildRirReference(ctx, prefix.Rir) + if err != nil { + return fmt.Errorf("failed to build rir reference for prefix %s: %w", prefix.Prefix, err) + } + prefixRequest.Rir = rirRef + } + + if prefix.DateAllocated != "" { + dateRef, err := s.parseDateAllocated(prefix.DateAllocated) + if err != nil { + return fmt.Errorf("failed to parse date_allocated for prefix %s: %w", prefix.Prefix, err) + } + prefixRequest.DateAllocated = *nb.NewNullableTime(&dateRef) + } + + if len(prefix.Locations) > 0 { + locationRef, err := s.buildLocationReference(ctx, prefix.Locations[0]) + if err != nil { + return fmt.Errorf("failed to build location reference for prefix %s: %w", prefix.Prefix, err) + } + prefixRequest.Location = locationRef + } + + if prefix.Vlan != "" { + prefixRequest.Vlan = s.buildVlanReference(ctx, prefix.Vlan) + } + + if prefix.Tenant != "" { + prefixRequest.Tenant = s.buildTenantReference(ctx, prefix.Tenant) + } + + customFields := make(map[string]interface{}) + if prefix.TenantGroup != "" { + customFields["tenant_group"] = s.buildTenantGroupID(ctx, prefix.TenantGroup) + } + if prefix.VlanGroup != "" { + customFields["vlan_group"] = s.buildVlanGroupID(ctx, prefix.VlanGroup) + } + if len(prefix.Vrfs) > 0 { + customFields["vrfs"] = s.buildVrfIDs(ctx, prefix.Vrfs) + } + if len(prefix.Locations) > 1 { + customFields["locations"] = s.buildLocationIDs(ctx, prefix.Locations[1:]) + } + if len(customFields) > 0 { + prefixRequest.CustomFields = customFields + } + + if len(prefix.Tags) > 0 { + prefixRequest.Tags = s.buildTagReferences(ctx, prefix.Tags) + } + + if existingPrefix.Id == nil { + return s.createPrefix(ctx, prefixRequest) + } + + if !helpers.CompareJSONFields(existingPrefix, prefixRequest) { + return s.updatePrefix(ctx, *existingPrefix.Id, prefixRequest) + } + + log.Info("prefix unchanged, skipping update", "prefix", prefixRequest.Prefix) + return nil +} + +// createPrefix creates a new prefix in Nautobot +func (s *PrefixSync) createPrefix(ctx context.Context, request nb.WritablePrefixRequest) error { + createdPrefix, err := s.prefixSvc.Create(ctx, request) + if err != nil || createdPrefix == nil { + return fmt.Errorf("failed to create prefix %s: %w", request.Prefix, err) + } + log.Info("prefix created", "prefix", request.Prefix) + return nil +} + +// updatePrefix updates an existing prefix in Nautobot +func (s *PrefixSync) updatePrefix(ctx context.Context, id string, request nb.WritablePrefixRequest) error { + updatedPrefix, err := s.prefixSvc.Update(ctx, id, request) + if err != nil || updatedPrefix == nil { + return fmt.Errorf("failed to update prefix %s: %w", request.Prefix, err) + } + log.Info("prefix updated", "prefix", request.Prefix) + return nil +} + +// deleteObsoletePrefixes removes prefixes that are not defined in YAML +func (s *PrefixSync) deleteObsoletePrefixes(ctx context.Context, prefixes models.Prefixes) { + desiredPrefixes := make(map[string]models.Prefix) + for _, prefix := range prefixes.Prefix { + desiredPrefixes[prefix.Prefix] = prefix + } + + existingPrefixes := s.prefixSvc.ListAll(ctx) + existingMap := make(map[string]nb.Prefix, len(existingPrefixes)) + for _, prefix := range existingPrefixes { + existingMap[prefix.Prefix] = prefix + } + + obsoletePrefixes := lo.OmitByKeys(existingMap, lo.Keys(desiredPrefixes)) + for _, prefix := range obsoletePrefixes { + if prefix.Id != nil { + _ = s.prefixSvc.Destroy(ctx, *prefix.Id) + } + } +} + +func (s *PrefixSync) buildStatusReference(ctx context.Context, name string) (nb.ApprovalWorkflowStageResponseApprovalWorkflowStage, error) { + status := s.statusSvc.GetByName(ctx, name) + if status.Id == nil { + return nb.ApprovalWorkflowStageResponseApprovalWorkflowStage{}, fmt.Errorf("status '%s' not found in Nautobot", name) + } + return helpers.BuildApprovalWorkflowStageResponseApprovalWorkflowStage(*status.Id), nil +} + +func (s *PrefixSync) buildNamespaceReference(ctx context.Context, name string) (*nb.ApprovalWorkflowStageResponseApprovalWorkflowStage, error) { + ns := s.namespaceSvc.GetByName(ctx, name) + if ns.Id == nil { + return nil, fmt.Errorf("namespace '%s' not found in Nautobot", name) + } + ref := helpers.BuildApprovalWorkflowStageResponseApprovalWorkflowStage(*ns.Id) + return &ref, nil +} + +func (s *PrefixSync) buildRoleReference(ctx context.Context, name string) (nb.NullableApprovalWorkflowUser, error) { + role := s.roleSvc.GetByName(ctx, name) + if role.Id == nil { + return nb.NullableApprovalWorkflowUser{}, fmt.Errorf("role '%s' not found in Nautobot", name) + } + return helpers.BuildNullableApprovalWorkflowUser(*role.Id), nil +} + +func (s *PrefixSync) buildRirReference(ctx context.Context, name string) (nb.NullableBulkWritablePrefixRequestRir, error) { + rir := s.rirSvc.GetByName(ctx, name) + if rir.Id == nil { + return nb.NullableBulkWritablePrefixRequestRir{}, fmt.Errorf("rir '%s' not found in Nautobot", name) + } + return helpers.BuildNullableBulkWritablePrefixRequestRir(*rir.Id), nil +} + +func (s *PrefixSync) parseDateAllocated(dateStr string) (time.Time, error) { + return time.Parse("2006-01-02 15:04:05", dateStr) +} + +func (s *PrefixSync) buildLocationReference(ctx context.Context, name string) (nb.NullableBulkWritablePrefixRequestLocation, error) { + location := s.locationSvc.GetByName(ctx, name) + if location.Id == nil { + return nb.NullableBulkWritablePrefixRequestLocation{}, fmt.Errorf("location '%s' not found in Nautobot", name) + } + return helpers.BuildNullableBulkWritablePrefixRequestLocation(*location.Id), nil +} + +func (s *PrefixSync) buildVlanReference(ctx context.Context, name string) nb.NullableApprovalWorkflowUser { + vlan := s.vlanSvc.GetByName(ctx, name) + if vlan.Id == nil { + return nb.NullableApprovalWorkflowUser{} + } + return helpers.BuildNullableApprovalWorkflowUser(*vlan.Id) +} + +func (s *PrefixSync) buildTenantReference(ctx context.Context, name string) nb.NullableApprovalWorkflowUser { + tenant := s.tenantSvc.GetByName(ctx, name) + if tenant.Id == nil { + return nb.NullableApprovalWorkflowUser{} + } + return helpers.BuildNullableApprovalWorkflowUser(*tenant.Id) +} + +func (s *PrefixSync) buildTenantGroupID(ctx context.Context, name string) string { + tg := s.tenantGroupSvc.GetByName(ctx, name) + if tg.Id == nil { + return "" + } + return *tg.Id +} + +func (s *PrefixSync) buildVlanGroupID(ctx context.Context, name string) string { + vg := s.vlanGroupSvc.GetByName(ctx, name) + if vg.Id == nil { + return "" + } + return *vg.Id +} + +func (s *PrefixSync) buildVrfIDs(ctx context.Context, names []string) []string { + var ids []string + for _, name := range names { + vrf := s.vrfSvc.GetByName(ctx, name) + if vrf.Id != nil { + ids = append(ids, *vrf.Id) + } + } + return ids +} + +func (s *PrefixSync) buildLocationIDs(ctx context.Context, names []string) []string { + var ids []string + for _, name := range names { + location := s.locationSvc.GetByName(ctx, name) + if location.Id != nil { + ids = append(ids, *location.Id) + } + } + return ids +} + +func (s *PrefixSync) buildTagReferences(ctx context.Context, tagNames []string) []nb.ApprovalWorkflowStageResponseApprovalWorkflowStage { + var tags []nb.ApprovalWorkflowStageResponseApprovalWorkflowStage + for _, name := range tagNames { + tag := s.tagSvc.GetByName(ctx, name) + if tag.Id != nil { + tags = append(tags, helpers.BuildApprovalWorkflowStageResponseApprovalWorkflowStage(*tag.Id)) + } + } + return tags +} diff --git a/go/nautobotop/internal/nautobot/sync/rir.go b/go/nautobotop/internal/nautobot/sync/rir.go new file mode 100644 index 000000000..595452082 --- /dev/null +++ b/go/nautobotop/internal/nautobot/sync/rir.go @@ -0,0 +1,106 @@ +package sync + +import ( + "context" + "fmt" + + "github.com/charmbracelet/log" + nb "github.com/nautobot/go-nautobot/v3" + "github.com/rackerlabs/understack/go/nautobotop/internal/nautobot/client" + "github.com/rackerlabs/understack/go/nautobotop/internal/nautobot/helpers" + "github.com/rackerlabs/understack/go/nautobotop/internal/nautobot/ipam" + "github.com/rackerlabs/understack/go/nautobotop/internal/nautobot/models" + "github.com/samber/lo" + "go.yaml.in/yaml/v3" +) + +type RirSync struct { + client *client.NautobotClient + rirSvc *ipam.RirService +} + +func NewRirSync(nautobotClient *client.NautobotClient) *RirSync { + return &RirSync{ + client: nautobotClient.GetClient(), + rirSvc: ipam.NewRirService(nautobotClient), + } +} + +func (s *RirSync) SyncAll(ctx context.Context, data map[string]string) error { + var rirs models.Rirs + for key, f := range data { + var yml []models.Rir + if err := yaml.Unmarshal([]byte(f), &yml); err != nil { + s.client.AddReport("yamlFailed", "file: "+key+" error: "+err.Error()) + return err + } + rirs.Rir = append(rirs.Rir, yml...) + } + + for _, rir := range rirs.Rir { + if err := s.syncSingleRir(ctx, rir); err != nil { + return err + } + } + s.deleteObsoleteRirs(ctx, rirs) + return nil +} + +func (s *RirSync) syncSingleRir(ctx context.Context, rir models.Rir) error { + existingRir := s.rirSvc.GetByName(ctx, rir.Name) + + rirRequest := nb.RIRRequest{ + Name: rir.Name, + IsPrivate: nb.PtrBool(rir.IsPrivate), + } + if rir.Description != "" { + rirRequest.Description = nb.PtrString(rir.Description) + } + + if existingRir.Id == nil { + return s.createRir(ctx, rirRequest) + } + if !helpers.CompareJSONFields(existingRir, rirRequest) { + return s.updateRir(ctx, *existingRir.Id, rirRequest) + } + log.Info("rir unchanged, skipping update", "name", rirRequest.Name) + return nil +} + +func (s *RirSync) createRir(ctx context.Context, request nb.RIRRequest) error { + created, err := s.rirSvc.Create(ctx, request) + if err != nil || created == nil { + return fmt.Errorf("failed to create rir %s: %w", request.Name, err) + } + log.Info("rir created", "name", request.Name) + return nil +} + +func (s *RirSync) updateRir(ctx context.Context, id string, request nb.RIRRequest) error { + updated, err := s.rirSvc.Update(ctx, id, request) + if err != nil || updated == nil { + return fmt.Errorf("failed to update rir %s: %w", request.Name, err) + } + log.Info("rir updated", "name", request.Name) + return nil +} + +func (s *RirSync) deleteObsoleteRirs(ctx context.Context, rirs models.Rirs) { + desired := make(map[string]models.Rir) + for _, rir := range rirs.Rir { + desired[rir.Name] = rir + } + + existing := s.rirSvc.ListAll(ctx) + existingMap := make(map[string]nb.RIR, len(existing)) + for _, rir := range existing { + existingMap[rir.Name] = rir + } + + obsolete := lo.OmitByKeys(existingMap, lo.Keys(desired)) + for _, rir := range obsolete { + if rir.Id != nil { + _ = s.rirSvc.Destroy(ctx, *rir.Id) + } + } +} diff --git a/go/nautobotop/internal/nautobot/sync/role.go b/go/nautobotop/internal/nautobot/sync/role.go new file mode 100644 index 000000000..7549d0732 --- /dev/null +++ b/go/nautobotop/internal/nautobot/sync/role.go @@ -0,0 +1,112 @@ +package sync + +import ( + "context" + "fmt" + + "github.com/charmbracelet/log" + nb "github.com/nautobot/go-nautobot/v3" + "github.com/rackerlabs/understack/go/nautobotop/internal/nautobot/client" + "github.com/rackerlabs/understack/go/nautobotop/internal/nautobot/extras" + "github.com/rackerlabs/understack/go/nautobotop/internal/nautobot/helpers" + "github.com/rackerlabs/understack/go/nautobotop/internal/nautobot/models" + "github.com/samber/lo" + "go.yaml.in/yaml/v3" +) + +type RoleSync struct { + client *client.NautobotClient + roleSvc *extras.RoleService +} + +func NewRoleSync(nautobotClient *client.NautobotClient) *RoleSync { + return &RoleSync{ + client: nautobotClient.GetClient(), + roleSvc: extras.NewRoleService(nautobotClient), + } +} + +func (s *RoleSync) SyncAll(ctx context.Context, data map[string]string) error { + var roles models.Roles + for key, f := range data { + var yml []models.Role + if err := yaml.Unmarshal([]byte(f), &yml); err != nil { + s.client.AddReport("yamlFailed", "file: "+key+" error: "+err.Error()) + return err + } + roles.Role = append(roles.Role, yml...) + } + + for _, role := range roles.Role { + if err := s.syncSingleRole(ctx, role); err != nil { + return err + } + } + s.deleteObsoleteRoles(ctx, roles) + return nil +} + +func (s *RoleSync) syncSingleRole(ctx context.Context, role models.Role) error { + existing := s.roleSvc.GetByName(ctx, role.Name) + + roleRequest := nb.RoleRequest{ + Name: role.Name, + ContentTypes: role.ContentTypes, + } + if role.Description != "" { + roleRequest.Description = nb.PtrString(role.Description) + } + if role.Color != "" { + roleRequest.Color = nb.PtrString(role.Color) + } + if role.Weight > 0 { + roleRequest.Weight = *nb.NewNullableInt32(nb.PtrInt32(int32(role.Weight))) + } + + if existing.Id == nil { + return s.createRole(ctx, roleRequest) + } + if !helpers.CompareJSONFields(existing, roleRequest) { + return s.updateRole(ctx, *existing.Id, roleRequest) + } + log.Info("role unchanged, skipping update", "name", roleRequest.Name) + return nil +} + +func (s *RoleSync) createRole(ctx context.Context, request nb.RoleRequest) error { + created, err := s.roleSvc.Create(ctx, request) + if err != nil || created == nil { + return fmt.Errorf("failed to create role %s: %w", request.Name, err) + } + log.Info("role created", "name", request.Name) + return nil +} + +func (s *RoleSync) updateRole(ctx context.Context, id string, request nb.RoleRequest) error { + updated, err := s.roleSvc.Update(ctx, id, request) + if err != nil || updated == nil { + return fmt.Errorf("failed to update role %s: %w", request.Name, err) + } + log.Info("role updated", "name", request.Name) + return nil +} + +func (s *RoleSync) deleteObsoleteRoles(ctx context.Context, roles models.Roles) { + desired := make(map[string]models.Role) + for _, role := range roles.Role { + desired[role.Name] = role + } + + existing := s.roleSvc.ListAll(ctx) + existingMap := make(map[string]nb.Role, len(existing)) + for _, role := range existing { + existingMap[role.Name] = role + } + + obsolete := lo.OmitByKeys(existingMap, lo.Keys(desired)) + for _, role := range obsolete { + if role.Id != nil { + _ = s.roleSvc.Destroy(ctx, *role.Id) + } + } +} diff --git a/go/nautobotop/internal/nautobot/sync/tenant.go b/go/nautobotop/internal/nautobot/sync/tenant.go new file mode 100644 index 000000000..864878625 --- /dev/null +++ b/go/nautobotop/internal/nautobot/sync/tenant.go @@ -0,0 +1,138 @@ +package sync + +import ( + "context" + "fmt" + + "github.com/charmbracelet/log" + nb "github.com/nautobot/go-nautobot/v3" + "github.com/rackerlabs/understack/go/nautobotop/internal/nautobot/client" + "github.com/rackerlabs/understack/go/nautobotop/internal/nautobot/extras" + "github.com/rackerlabs/understack/go/nautobotop/internal/nautobot/helpers" + "github.com/rackerlabs/understack/go/nautobotop/internal/nautobot/models" + "github.com/rackerlabs/understack/go/nautobotop/internal/nautobot/tenancy" + "github.com/samber/lo" + "go.yaml.in/yaml/v3" +) + +type TenantSync struct { + client *client.NautobotClient + tenantSvc *tenancy.TenantService + tenantGroupSvc *tenancy.TenantGroupService + tagSvc *extras.TagService +} + +func NewTenantSync(nautobotClient *client.NautobotClient) *TenantSync { + return &TenantSync{ + client: nautobotClient.GetClient(), + tenantSvc: tenancy.NewTenantService(nautobotClient), + tenantGroupSvc: tenancy.NewTenantGroupService(nautobotClient), + tagSvc: extras.NewTagService(nautobotClient), + } +} + +func (s *TenantSync) SyncAll(ctx context.Context, data map[string]string) error { + var tenants models.Tenants + for key, f := range data { + var yml []models.Tenant + if err := yaml.Unmarshal([]byte(f), &yml); err != nil { + s.client.AddReport("yamlFailed", "file: "+key+" error: "+err.Error()) + return err + } + tenants.Tenant = append(tenants.Tenant, yml...) + } + + for _, t := range tenants.Tenant { + if err := s.syncSingleTenant(ctx, t); err != nil { + return err + } + } + s.deleteObsoleteTenants(ctx, tenants) + return nil +} + +func (s *TenantSync) syncSingleTenant(ctx context.Context, tenant models.Tenant) error { + existing := s.tenantSvc.GetByName(ctx, tenant.Name) + + tenantRequest := nb.TenantRequest{ + Name: tenant.Name, + } + if tenant.Description != "" { + tenantRequest.Description = nb.PtrString(tenant.Description) + } + if tenant.Comments != "" { + tenantRequest.Comments = nb.PtrString(tenant.Comments) + } + if tenant.TenantGroup != "" { + tenantRequest.TenantGroup = s.buildTenantGroupReference(ctx, tenant.TenantGroup) + } + if len(tenant.Tags) > 0 { + tenantRequest.Tags = s.buildTagReferences(ctx, tenant.Tags) + } + + if existing.Id == nil { + return s.createTenant(ctx, tenantRequest) + } + if !helpers.CompareJSONFields(existing, tenantRequest) { + return s.updateTenant(ctx, *existing.Id, tenantRequest) + } + log.Info("tenant unchanged, skipping update", "name", tenantRequest.Name) + return nil +} + +func (s *TenantSync) createTenant(ctx context.Context, request nb.TenantRequest) error { + created, err := s.tenantSvc.Create(ctx, request) + if err != nil || created == nil { + return fmt.Errorf("failed to create tenant %s: %w", request.Name, err) + } + log.Info("tenant created", "name", request.Name) + return nil +} + +func (s *TenantSync) updateTenant(ctx context.Context, id string, request nb.TenantRequest) error { + updated, err := s.tenantSvc.Update(ctx, id, request) + if err != nil || updated == nil { + return fmt.Errorf("failed to update tenant %s: %w", request.Name, err) + } + log.Info("tenant updated", "name", request.Name) + return nil +} + +func (s *TenantSync) deleteObsoleteTenants(ctx context.Context, tenants models.Tenants) { + desired := make(map[string]models.Tenant) + for _, t := range tenants.Tenant { + desired[t.Name] = t + } + + existing := s.tenantSvc.ListAll(ctx) + existingMap := make(map[string]nb.Tenant, len(existing)) + for _, t := range existing { + existingMap[t.Name] = t + } + + obsolete := lo.OmitByKeys(existingMap, lo.Keys(desired)) + for _, t := range obsolete { + if t.Id != nil { + _ = s.tenantSvc.Destroy(ctx, *t.Id) + } + } +} + +func (s *TenantSync) buildTenantGroupReference(ctx context.Context, name string) nb.NullableApprovalWorkflowUser { + tg := s.tenantGroupSvc.GetByName(ctx, name) + if tg.Id == nil { + return nb.NullableApprovalWorkflowUser{} + } + return helpers.BuildNullableApprovalWorkflowUser(*tg.Id) +} + +func (s *TenantSync) buildTagReferences(ctx context.Context, tagNames []string) []nb.ApprovalWorkflowStageResponseApprovalWorkflowStage { + var tags []nb.ApprovalWorkflowStageResponseApprovalWorkflowStage + for _, name := range tagNames { + tag := s.tagSvc.GetByName(ctx, name) + if tag.Id != nil { + tags = append(tags, helpers.BuildApprovalWorkflowStageResponseApprovalWorkflowStage(*tag.Id)) + } + } + return tags +} diff --git a/go/nautobotop/internal/nautobot/sync/tenantGroup.go b/go/nautobotop/internal/nautobot/sync/tenantGroup.go new file mode 100644 index 000000000..e2d064b58 --- /dev/null +++ b/go/nautobotop/internal/nautobot/sync/tenantGroup.go @@ -0,0 +1,116 @@ +package sync + +import ( + "context" + "fmt" + + "github.com/charmbracelet/log" + nb "github.com/nautobot/go-nautobot/v3" + "github.com/rackerlabs/understack/go/nautobotop/internal/nautobot/client" + "github.com/rackerlabs/understack/go/nautobotop/internal/nautobot/helpers" + "github.com/rackerlabs/understack/go/nautobotop/internal/nautobot/models" + "github.com/rackerlabs/understack/go/nautobotop/internal/nautobot/tenancy" + "github.com/samber/lo" + "go.yaml.in/yaml/v3" +) + +type TenantGroupSync struct { + client *client.NautobotClient + tenantGroupSvc *tenancy.TenantGroupService +} + +func NewTenantGroupSync(nautobotClient *client.NautobotClient) *TenantGroupSync { + return &TenantGroupSync{ + client: nautobotClient.GetClient(), + tenantGroupSvc: tenancy.NewTenantGroupService(nautobotClient), + } +} + +func (s *TenantGroupSync) SyncAll(ctx context.Context, data map[string]string) error { + var tenantGroups models.TenantGroups + for key, f := range data { + var yml []models.TenantGroup + if err := yaml.Unmarshal([]byte(f), &yml); err != nil { + s.client.AddReport("yamlFailed", "file: "+key+" error: "+err.Error()) + return err + } + tenantGroups.TenantGroup = append(tenantGroups.TenantGroup, yml...) + } + + for _, tg := range tenantGroups.TenantGroup { + if err := s.syncSingleTenantGroup(ctx, tg); err != nil { + return err + } + } + s.deleteObsoleteTenantGroups(ctx, tenantGroups) + return nil +} + +func (s *TenantGroupSync) syncSingleTenantGroup(ctx context.Context, tg models.TenantGroup) error { + existing := s.tenantGroupSvc.GetByName(ctx, tg.Name) + + tgRequest := nb.TenantGroupRequest{ + Name: tg.Name, + } + if tg.Description != "" { + tgRequest.Description = nb.PtrString(tg.Description) + } + if tg.Parent != "" { + tgRequest.Parent = s.buildParentReference(ctx, tg.Parent) + } + + if existing.Id == nil { + return s.createTenantGroup(ctx, tgRequest) + } + if !helpers.CompareJSONFields(existing, tgRequest) { + return s.updateTenantGroup(ctx, *existing.Id, tgRequest) + } + log.Info("tenant group unchanged, skipping update", "name", tgRequest.Name) + return nil +} + +func (s *TenantGroupSync) createTenantGroup(ctx context.Context, request nb.TenantGroupRequest) error { + created, err := s.tenantGroupSvc.Create(ctx, request) + if err != nil || created == nil { + return fmt.Errorf("failed to create tenant group %s: %w", request.Name, err) + } + log.Info("tenant group created", "name", request.Name) + return nil +} + +func (s *TenantGroupSync) updateTenantGroup(ctx context.Context, id string, request nb.TenantGroupRequest) error { + updated, err := s.tenantGroupSvc.Update(ctx, id, request) + if err != nil || updated == nil { + return fmt.Errorf("failed to update tenant group %s: %w", request.Name, err) + } + log.Info("tenant group updated", "name", request.Name) + return nil +} + +func (s *TenantGroupSync) deleteObsoleteTenantGroups(ctx context.Context, tenantGroups models.TenantGroups) { + desired := make(map[string]models.TenantGroup) + for _, tg := range tenantGroups.TenantGroup { + desired[tg.Name] = tg + } + + existing := s.tenantGroupSvc.ListAll(ctx) + existingMap := make(map[string]nb.TenantGroup, len(existing)) + for _, tg := range existing { + existingMap[tg.Name] = tg + } + + obsolete := lo.OmitByKeys(existingMap, lo.Keys(desired)) + for _, tg := range obsolete { + if tg.Id != nil { + _ = s.tenantGroupSvc.Destroy(ctx, *tg.Id) + } + } +} + +func (s *TenantGroupSync) buildParentReference(ctx context.Context, name string) nb.NullableApprovalWorkflowUser { + parent := s.tenantGroupSvc.GetByName(ctx, name) + if parent.Id == nil { + return nb.NullableApprovalWorkflowUser{} + } + return helpers.BuildNullableApprovalWorkflowUser(*parent.Id) +} diff --git a/go/nautobotop/internal/nautobot/sync/vlan.go b/go/nautobotop/internal/nautobot/sync/vlan.go new file mode 100644 index 000000000..bc32896b2 --- /dev/null +++ b/go/nautobotop/internal/nautobot/sync/vlan.go @@ -0,0 +1,254 @@ +package sync + +import ( + "context" + "fmt" + + "github.com/charmbracelet/log" + nb "github.com/nautobot/go-nautobot/v3" + "github.com/rackerlabs/understack/go/nautobotop/internal/nautobot/client" + "github.com/rackerlabs/understack/go/nautobotop/internal/nautobot/dcim" + "github.com/rackerlabs/understack/go/nautobotop/internal/nautobot/extras" + "github.com/rackerlabs/understack/go/nautobotop/internal/nautobot/helpers" + "github.com/rackerlabs/understack/go/nautobotop/internal/nautobot/ipam" + "github.com/rackerlabs/understack/go/nautobotop/internal/nautobot/models" + "github.com/rackerlabs/understack/go/nautobotop/internal/nautobot/tenancy" + "github.com/samber/lo" + "go.yaml.in/yaml/v3" +) + +type VlanSync struct { + client *client.NautobotClient + vlanSvc *ipam.VlanService + vlanGroupSvc *ipam.VlanGroupService + locationSvc *dcim.LocationService + statusSvc *dcim.StatusService + roleSvc *extras.RoleService + tenantSvc *tenancy.TenantService + tenantGroupSvc *tenancy.TenantGroupService + tagSvc *extras.TagService +} + +func NewVlanSync(nautobotClient *client.NautobotClient) *VlanSync { + return &VlanSync{ + client: nautobotClient.GetClient(), + vlanSvc: ipam.NewVlanService(nautobotClient), + vlanGroupSvc: ipam.NewVlanGroupService(nautobotClient), + locationSvc: dcim.NewLocationService(nautobotClient.GetClient()), + statusSvc: dcim.NewStatusService(nautobotClient.GetClient()), + roleSvc: extras.NewRoleService(nautobotClient), + tenantSvc: tenancy.NewTenantService(nautobotClient), + tenantGroupSvc: tenancy.NewTenantGroupService(nautobotClient), + tagSvc: extras.NewTagService(nautobotClient), + } +} + +func (s *VlanSync) SyncAll(ctx context.Context, data map[string]string) error { + var vlans models.Vlans + for key, f := range data { + var yml []models.Vlan + if err := yaml.Unmarshal([]byte(f), &yml); err != nil { + s.client.AddReport("yamlFailed", "file: "+key+" error: "+err.Error()) + return err + } + vlans.Vlan = append(vlans.Vlan, yml...) + } + + for _, vlan := range vlans.Vlan { + if err := s.syncSingleVlan(ctx, vlan); err != nil { + return err + } + } + s.deleteObsoleteVlans(ctx, vlans) + + return nil +} + +// syncSingleVlan handles the create/update logic for a single VLAN +func (s *VlanSync) syncSingleVlan(ctx context.Context, vlan models.Vlan) error { + existingVlan := s.vlanSvc.GetByName(ctx, vlan.Name) + + // Build status reference (required) + statusRef, err := s.buildStatusReference(ctx, vlan.Status) + if err != nil { + return fmt.Errorf("failed to build status reference for vlan %s: %w", vlan.Name, err) + } + + vlanRequest := nb.VLANRequest{ + Vid: int32(vlan.Vid), + Name: vlan.Name, + Status: statusRef, + } + + if vlan.Description != "" { + vlanRequest.Description = nb.PtrString(vlan.Description) + } + + if vlan.Role != "" { + roleRef, err := s.buildRoleReference(ctx, vlan.Role) + if err != nil { + return fmt.Errorf("failed to build role reference for vlan %s: %w", vlan.Name, err) + } + vlanRequest.Role = roleRef + } + + if len(vlan.Locations) > 0 { + locationRef, err := s.buildLocationReference(ctx, vlan.Locations[0]) + if err != nil { + return fmt.Errorf("failed to build location reference for vlan %s: %w", vlan.Name, err) + } + vlanRequest.Location = locationRef + } + + if vlan.VlanGroup != "" { + vlanRequest.VlanGroup = s.buildVlanGroupReference(ctx, vlan.VlanGroup) + } + + if vlan.Tenant != "" { + vlanRequest.Tenant = s.buildTenantReference(ctx, vlan.Tenant) + } + + customFields := make(map[string]interface{}) + if vlan.TenantGroup != "" { + customFields["tenant_group"] = s.buildTenantGroupID(ctx, vlan.TenantGroup) + } + if len(vlan.DynamicGroups) > 0 { + customFields["dynamic_groups"] = s.buildDynamicGroupNames(vlan.DynamicGroups) + } + if len(vlan.Locations) > 1 { + customFields["locations"] = s.buildLocationIDs(ctx, vlan.Locations[1:]) + } + if len(customFields) > 0 { + vlanRequest.CustomFields = customFields + } + + if len(vlan.Tags) > 0 { + vlanRequest.Tags = s.buildTagReferences(ctx, vlan.Tags) + } + + if existingVlan.Id == nil { + return s.createVlan(ctx, vlanRequest) + } + + if !helpers.CompareJSONFields(existingVlan, vlanRequest) { + return s.updateVlan(ctx, *existingVlan.Id, vlanRequest) + } + + log.Info("vlan unchanged, skipping update", "name", vlanRequest.Name) + return nil +} + +// createVlan creates a new VLAN in Nautobot +func (s *VlanSync) createVlan(ctx context.Context, request nb.VLANRequest) error { + createdVlan, err := s.vlanSvc.Create(ctx, request) + if err != nil || createdVlan == nil { + return fmt.Errorf("failed to create vlan %s: %w", request.Name, err) + } + log.Info("vlan created", "name", request.Name) + return nil +} + +// updateVlan updates an existing VLAN in Nautobot +func (s *VlanSync) updateVlan(ctx context.Context, id string, request nb.VLANRequest) error { + updatedVlan, err := s.vlanSvc.Update(ctx, id, request) + if err != nil || updatedVlan == nil { + return fmt.Errorf("failed to update vlan %s: %w", request.Name, err) + } + log.Info("vlan updated", "name", request.Name) + return nil +} + +// deleteObsoleteVlans removes VLANs that are not defined in YAML +func (s *VlanSync) deleteObsoleteVlans(ctx context.Context, vlans models.Vlans) { + desiredVlans := make(map[string]models.Vlan) + for _, vlan := range vlans.Vlan { + desiredVlans[vlan.Name] = vlan + } + + existingVlans := s.vlanSvc.ListAll(ctx) + existingMap := make(map[string]nb.VLAN, len(existingVlans)) + for _, vlan := range existingVlans { + existingMap[vlan.Name] = vlan + } + + obsoleteVlans := lo.OmitByKeys(existingMap, lo.Keys(desiredVlans)) + for _, vlan := range obsoleteVlans { + if vlan.Id != nil { + _ = s.vlanSvc.Destroy(ctx, *vlan.Id) + } + } +} + +func (s *VlanSync) buildStatusReference(ctx context.Context, name string) (nb.ApprovalWorkflowStageResponseApprovalWorkflowStage, error) { + status := s.statusSvc.GetByName(ctx, name) + if status.Id == nil { + return nb.ApprovalWorkflowStageResponseApprovalWorkflowStage{}, fmt.Errorf("status '%s' not found in Nautobot", name) + } + return helpers.BuildApprovalWorkflowStageResponseApprovalWorkflowStage(*status.Id), nil +} + +func (s *VlanSync) buildRoleReference(ctx context.Context, name string) (nb.NullableApprovalWorkflowUser, error) { + role := s.roleSvc.GetByName(ctx, name) + if role.Id == nil { + return nb.NullableApprovalWorkflowUser{}, fmt.Errorf("role '%s' not found in Nautobot", name) + } + return helpers.BuildNullableApprovalWorkflowUser(*role.Id), nil +} + +func (s *VlanSync) buildLocationReference(ctx context.Context, name string) (nb.NullableBulkWritablePrefixRequestLocation, error) { + location := s.locationSvc.GetByName(ctx, name) + if location.Id == nil { + return nb.NullableBulkWritablePrefixRequestLocation{}, fmt.Errorf("location '%s' not found in Nautobot", name) + } + return helpers.BuildNullableBulkWritablePrefixRequestLocation(*location.Id), nil +} + +func (s *VlanSync) buildVlanGroupReference(ctx context.Context, name string) nb.NullableApprovalWorkflowUser { + vlanGroup := s.vlanGroupSvc.GetByName(ctx, name) + if vlanGroup.Id == nil { + return nb.NullableApprovalWorkflowUser{} + } + return helpers.BuildNullableApprovalWorkflowUser(*vlanGroup.Id) +} + +func (s *VlanSync) buildTenantReference(ctx context.Context, name string) nb.NullableApprovalWorkflowUser { + tenant := s.tenantSvc.GetByName(ctx, name) + if tenant.Id == nil { + return nb.NullableApprovalWorkflowUser{} + } + return helpers.BuildNullableApprovalWorkflowUser(*tenant.Id) +} + +func (s *VlanSync) buildTenantGroupID(ctx context.Context, name string) string { + tg := s.tenantGroupSvc.GetByName(ctx, name) + if tg.Id == nil { + return "" + } + return *tg.Id +} + +func (s *VlanSync) buildDynamicGroupNames(names []string) []string { + return names +} + +func (s *VlanSync) buildLocationIDs(ctx context.Context, names []string) []string { + var ids []string + for _, name := range names { + location := s.locationSvc.GetByName(ctx, name) + if location.Id != nil { + ids = append(ids, *location.Id) + } + } + return ids +} + +func (s *VlanSync) buildTagReferences(ctx context.Context, tagNames []string) []nb.ApprovalWorkflowStageResponseApprovalWorkflowStage { + var tags []nb.ApprovalWorkflowStageResponseApprovalWorkflowStage + for _, name := range tagNames { + tag := s.tagSvc.GetByName(ctx, name) + if tag.Id != nil { + tags = append(tags, helpers.BuildApprovalWorkflowStageResponseApprovalWorkflowStage(*tag.Id)) + } + } + return tags +} diff --git a/go/nautobotop/internal/nautobot/tenancy/tenant.go b/go/nautobotop/internal/nautobot/tenancy/tenant.go index 2117961bb..5bb54c008 100644 --- a/go/nautobotop/internal/nautobot/tenancy/tenant.go +++ b/go/nautobotop/internal/nautobot/tenancy/tenant.go @@ -6,6 +6,7 @@ import ( "github.com/rackerlabs/understack/go/nautobotop/internal/nautobot/cache" "github.com/rackerlabs/understack/go/nautobotop/internal/nautobot/client" + "github.com/charmbracelet/log" nb "github.com/nautobot/go-nautobot/v3" "github.com/rackerlabs/understack/go/nautobotop/internal/nautobot/helpers" ) @@ -20,6 +21,18 @@ func NewTenantService(nautobotClient *client.NautobotClient) *TenantService { } } +func (s *TenantService) Create(ctx context.Context, req nb.TenantRequest) (*nb.Tenant, error) { + tenant, resp, err := s.client.APIClient.TenancyAPI.TenancyTenantsCreate(ctx).TenantRequest(req).Execute() + if err != nil { + bodyString := helpers.ReadResponseBody(resp) + s.client.AddReport("createNewTenant", "failed to create", "model", req.Name, "error", err.Error(), "response_body", bodyString) + return nil, err + } + log.Info("CreateTenant", "created tenant", tenant.Name) + cache.AddToCollection(s.client.Cache, "tenants", *tenant) + return tenant, nil +} + func (s *TenantService) GetByName(ctx context.Context, name string) nb.Tenant { if tenant, ok := cache.FindByName(s.client.Cache, "tenants", name, func(t nb.Tenant) string { return t.Name @@ -42,3 +55,47 @@ func (s *TenantService) GetByName(ctx context.Context, name string) nb.Tenant { return list.Results[0] } + +func (s *TenantService) ListAll(ctx context.Context) []nb.Tenant { + ids := s.client.GetChangeObjectIDS(ctx, "tenancy.tenant") + list, resp, err := s.client.APIClient.TenancyAPI.TenancyTenantsList(ctx).Id(ids).Depth(2).Execute() + if err != nil { + bodyString := helpers.ReadResponseBody(resp) + s.client.AddReport("ListAllTenants", "failed to list", "error", err.Error(), "response_body", bodyString) + return []nb.Tenant{} + } + if list == nil || len(list.Results) == 0 { + return []nb.Tenant{} + } + if list.Results[0].Id == nil { + return []nb.Tenant{} + } + return list.Results +} + +func (s *TenantService) Update(ctx context.Context, id string, req nb.TenantRequest) (*nb.Tenant, error) { + tenant, resp, err := s.client.APIClient.TenancyAPI.TenancyTenantsUpdate(ctx, id).TenantRequest(req).Execute() + if err != nil { + bodyString := helpers.ReadResponseBody(resp) + s.client.AddReport("UpdateTenant", "failed to update", "id", id, "model", req.Name, "error", err.Error(), "response_body", bodyString) + return nil, err + } + log.Info("successfully updated tenant", "id", id, "model", tenant.GetName()) + cache.UpdateInCollection(s.client.Cache, "tenants", *tenant, func(t nb.Tenant) bool { + return t.Id != nil && *t.Id == id + }) + return tenant, nil +} + +func (s *TenantService) Destroy(ctx context.Context, id string) error { + resp, err := s.client.APIClient.TenancyAPI.TenancyTenantsDestroy(ctx, id).Execute() + if err != nil { + bodyString := helpers.ReadResponseBody(resp) + s.client.AddReport("DestroyTenant", "failed to destroy", "id", id, "error", err.Error(), "response_body", bodyString) + return err + } + cache.RemoveFromCollection(s.client.Cache, "tenants", func(t nb.Tenant) bool { + return t.Id != nil && *t.Id == id + }) + return nil +} diff --git a/go/nautobotop/internal/nautobot/tenancy/tenantGroup.go b/go/nautobotop/internal/nautobot/tenancy/tenantGroup.go index baa1f9458..c83616024 100644 --- a/go/nautobotop/internal/nautobot/tenancy/tenantGroup.go +++ b/go/nautobotop/internal/nautobot/tenancy/tenantGroup.go @@ -6,6 +6,7 @@ import ( "github.com/rackerlabs/understack/go/nautobotop/internal/nautobot/cache" "github.com/rackerlabs/understack/go/nautobotop/internal/nautobot/client" + "github.com/charmbracelet/log" nb "github.com/nautobot/go-nautobot/v3" "github.com/rackerlabs/understack/go/nautobotop/internal/nautobot/helpers" ) @@ -20,6 +21,18 @@ func NewTenantGroupService(nautobotClient *client.NautobotClient) *TenantGroupSe } } +func (s *TenantGroupService) Create(ctx context.Context, req nb.TenantGroupRequest) (*nb.TenantGroup, error) { + tg, resp, err := s.client.APIClient.TenancyAPI.TenancyTenantGroupsCreate(ctx).TenantGroupRequest(req).Execute() + if err != nil { + bodyString := helpers.ReadResponseBody(resp) + s.client.AddReport("createNewTenantGroup", "failed to create", "model", req.Name, "error", err.Error(), "response_body", bodyString) + return nil, err + } + log.Info("CreateTenantGroup", "created tenant group", tg.Name) + cache.AddToCollection(s.client.Cache, "tenantgroups", *tg) + return tg, nil +} + func (s *TenantGroupService) GetByName(ctx context.Context, name string) nb.TenantGroup { if tg, ok := cache.FindByName(s.client.Cache, "tenantgroups", name, func(t nb.TenantGroup) string { return t.Name @@ -42,3 +55,47 @@ func (s *TenantGroupService) GetByName(ctx context.Context, name string) nb.Tena return list.Results[0] } + +func (s *TenantGroupService) ListAll(ctx context.Context) []nb.TenantGroup { + ids := s.client.GetChangeObjectIDS(ctx, "tenancy.tenantgroup") + list, resp, err := s.client.APIClient.TenancyAPI.TenancyTenantGroupsList(ctx).Id(ids).Depth(2).Execute() + if err != nil { + bodyString := helpers.ReadResponseBody(resp) + s.client.AddReport("ListAllTenantGroups", "failed to list", "error", err.Error(), "response_body", bodyString) + return []nb.TenantGroup{} + } + if list == nil || len(list.Results) == 0 { + return []nb.TenantGroup{} + } + if list.Results[0].Id == nil { + return []nb.TenantGroup{} + } + return list.Results +} + +func (s *TenantGroupService) Update(ctx context.Context, id string, req nb.TenantGroupRequest) (*nb.TenantGroup, error) { + tg, resp, err := s.client.APIClient.TenancyAPI.TenancyTenantGroupsUpdate(ctx, id).TenantGroupRequest(req).Execute() + if err != nil { + bodyString := helpers.ReadResponseBody(resp) + s.client.AddReport("UpdateTenantGroup", "failed to update", "id", id, "model", req.Name, "error", err.Error(), "response_body", bodyString) + return nil, err + } + log.Info("successfully updated tenant group", "id", id, "model", tg.GetName()) + cache.UpdateInCollection(s.client.Cache, "tenantgroups", *tg, func(t nb.TenantGroup) bool { + return t.Id != nil && *t.Id == id + }) + return tg, nil +} + +func (s *TenantGroupService) Destroy(ctx context.Context, id string) error { + resp, err := s.client.APIClient.TenancyAPI.TenancyTenantGroupsDestroy(ctx, id).Execute() + if err != nil { + bodyString := helpers.ReadResponseBody(resp) + s.client.AddReport("DestroyTenantGroup", "failed to destroy", "id", id, "error", err.Error(), "response_body", bodyString) + return err + } + cache.RemoveFromCollection(s.client.Cache, "tenantgroups", func(t nb.TenantGroup) bool { + return t.Id != nil && *t.Id == id + }) + return nil +}