Skip to content
7 changes: 7 additions & 0 deletions api/v1/prefix_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@ type PrefixSpec struct {
//+kubebuilder:validation:XValidation:rule="self == oldSelf",message="Field 'tenant' is immutable"
Tenant string `json:"tenant,omitempty"`

// A list of tags that will be assigned to the resource in NetBox.
// Each tag may contain either the `name` or `slug` field (one of them is required).
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it's either name or slug we should add a XValidation rule for this. The rule could probably be on the Tag struct.

// Example:
// - name: tag1
// - slug: tag2
Tags []Tag `json:"tags,omitempty"`

// The NetBox Custom Fields that should be added to the resource in NetBox.
// Note that currently only Text Type is supported (GitHub #129)
// More info on NetBox Custom Fields:
Expand Down
27 changes: 27 additions & 0 deletions api/v1/tag_types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
Copyright 2024 Swisscom (Schweiz) AG.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package v1

type Tag struct {
// +optional
// Name of the tag
Name string `json:"name,omitempty"`

// +optional
// Slug of the tag
Slug string `json:"slug,omitempty"`
}
20 changes: 20 additions & 0 deletions api/v1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 17 additions & 0 deletions config/crd/bases/netbox.dev_prefixes.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,23 @@ spec:
x-kubernetes-validations:
- message: Field 'site' is required once set
rule: self == oldSelf || self != ''
tags:
description: |-
A list of tags that will be assigned to the resource in NetBox.
Each tag may contain either the `name` or `slug` field (one of them is required).
Example:
- name: tag1
- slug: tag2
items:
properties:
name:
description: Name of the tag
type: string
slug:
description: Slug of the tag
type: string
type: object
type: array
tenant:
description: |-
The NetBox Tenant to be assigned to this resource in NetBox. Use the `name` value instead of the `slug` value
Expand Down
4 changes: 4 additions & 0 deletions config/samples/netbox_v1_prefix.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,7 @@ spec:
comments: "your comments"
preserveInNetbox: true
prefix: "2.0.0.0/24"
tags:
- name: Alpga
- slug: golf
- name: Bravo
133 changes: 74 additions & 59 deletions gen/mock_interfaces/netbox_mocks.go

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions internal/controller/prefix_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,14 @@ func generateNetboxPrefixModelFromPrefixSpec(spec *netboxv1.PrefixSpec, req ctrl
}
}

convertedTags := make([]models.Tag, len(spec.Tags))
for i, t := range spec.Tags {
convertedTags[i] = models.Tag{
Name: t.Name,
Slug: t.Slug,
}
}

return &models.Prefix{
Prefix: spec.Prefix,
Metadata: &models.NetboxMetadata{
Expand All @@ -300,6 +308,7 @@ func generateNetboxPrefixModelFromPrefixSpec(spec *netboxv1.PrefixSpec, req ctrl
Description: req.NamespacedName.String() + " // " + spec.Description,
Site: spec.Site,
Tenant: spec.Tenant,
Tags: convertedTags,
},
}, nil
}
13 changes: 13 additions & 0 deletions pkg/netbox/api/prefix.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,19 @@ func (r *NetboxClient) ReserveOrUpdatePrefix(prefix *models.Prefix) (*netboxMode
desiredPrefix.Site = &siteDetails.Id
}

desiredPrefix.Tags = []*netboxModels.NestedTag{}
// if the prefix has tags, fetch the details of each tag and add them to the desired prefix
if prefix.Metadata != nil && len(prefix.Metadata.Tags) > 0 {

for _, tag := range prefix.Metadata.Tags {
tagDetails, err := r.GetTagDetails(tag.Name, tag.Slug)
if err != nil {
return nil, err
}
desiredPrefix.Tags = append(desiredPrefix.Tags, &netboxModels.NestedTag{ID: tagDetails.Id, Name: &tagDetails.Name, Slug: &tagDetails.Slug})
}
}

// create prefix since it doesn't exist
if len(responsePrefix.Payload.Results) == 0 {
return r.CreatePrefix(desiredPrefix)
Expand Down
55 changes: 55 additions & 0 deletions pkg/netbox/api/tags.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
Copyright 2024 Swisscom (Schweiz) AG.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package api

import (
"github.com/netbox-community/go-netbox/v3/netbox/client/extras"

"github.com/netbox-community/netbox-operator/pkg/netbox/models"
"github.com/netbox-community/netbox-operator/pkg/netbox/utils"
)

func (r *NetboxClient) GetTagDetails(name string, slug string) (*models.Tag, error) {
var request *extras.ExtrasTagsListParams
if name != "" {
request = extras.NewExtrasTagsListParams().WithName(&name)
}
if slug != "" {
request = extras.NewExtrasTagsListParams().WithSlug(&slug)
}

if name == "" && slug == "" {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This case should be prevented during creation on kube-api using XValidation rule in the api/CRD.

return nil, utils.NetboxError("either name or slug must be provided to fetch Tag details", nil)
}
// response, err := r.Tags.ExtrasTagsList(request, nil)
response, err := r.Extras.ExtrasTagsList(request, nil)
if err != nil {
return nil, utils.NetboxError("failed to fetch Tag details", err)
}

if len(response.Payload.Results) == 0 {
return nil, utils.NetboxNotFoundError("tag '" + name + "/" + slug + "'")
}

tag := response.Payload.Results[0]
return &models.Tag{
Id: tag.ID,
Name: *tag.Name,
Slug: *tag.Slug,
}, nil

}
136 changes: 136 additions & 0 deletions pkg/netbox/api/tags_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/*
Copyright 2024 Swisscom (Schweiz) AG.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package api

import (
"errors"
"testing"

"github.com/netbox-community/go-netbox/v3/netbox/client/extras"
netboxModels "github.com/netbox-community/go-netbox/v3/netbox/models"
"github.com/netbox-community/netbox-operator/gen/mock_interfaces"
"github.com/stretchr/testify/assert"
"go.uber.org/mock/gomock"
)

func TestTags_GetTagDetailsByName(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockExtras := mock_interfaces.NewMockExtrasInterface(ctrl)

tagName := "myTag"
tagSlug := "mytag"
tagId := int64(1)

tagListRequestInput := extras.NewExtrasTagsListParams().WithName(&tagName)
tagListOutput := &extras.ExtrasTagsListOK{
Payload: &extras.ExtrasTagsListOKBody{
Results: []*netboxModels.Tag{
{
ID: tagId,
Name: &tagName,
Slug: &tagSlug,
},
},
},
}

mockExtras.EXPECT().ExtrasTagsList(tagListRequestInput, nil).Return(tagListOutput, nil)
netboxClient := &NetboxClient{Extras: mockExtras}

actual, err := netboxClient.GetTagDetails(tagName, "")
assert.NoError(t, err)
assert.Equal(t, tagName, actual.Name)
assert.Equal(t, tagId, actual.Id)
assert.Equal(t, tagSlug, actual.Slug)
}

func TestTags_GetTagDetailsBySlug(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockExtras := mock_interfaces.NewMockExtrasInterface(ctrl)

tagName := "myTag"
tagSlug := "mytag"
tagId := int64(1)

tagListRequestInput := extras.NewExtrasTagsListParams().WithSlug(&tagSlug)
tagListOutput := &extras.ExtrasTagsListOK{
Payload: &extras.ExtrasTagsListOKBody{
Results: []*netboxModels.Tag{
{
ID: tagId,
Name: &tagName,
Slug: &tagSlug,
},
},
},
}

mockExtras.EXPECT().ExtrasTagsList(tagListRequestInput, nil).Return(tagListOutput, nil)
netboxClient := &NetboxClient{Extras: mockExtras}

actual, err := netboxClient.GetTagDetails("", tagSlug)
assert.NoError(t, err)
assert.Equal(t, tagName, actual.Name)
assert.Equal(t, tagId, actual.Id)
assert.Equal(t, tagSlug, actual.Slug)
}

func TestTags_GetTagDetailsNotFound(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockExtras := mock_interfaces.NewMockExtrasInterface(ctrl)

tagName := "notfound"
tagListRequestInput := extras.NewExtrasTagsListParams().WithName(&tagName)
tagListOutput := &extras.ExtrasTagsListOK{
Payload: &extras.ExtrasTagsListOKBody{
Results: []*netboxModels.Tag{},
},
}

mockExtras.EXPECT().ExtrasTagsList(tagListRequestInput, nil).Return(tagListOutput, nil)
netboxClient := &NetboxClient{Extras: mockExtras}

actual, err := netboxClient.GetTagDetails(tagName, "")
assert.Nil(t, actual)
assert.EqualError(t, err, "failed to fetch tag 'notfound/': not found")
}

func TestTags_GetTagDetailsError(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockExtras := mock_interfaces.NewMockExtrasInterface(ctrl)

tagName := "error"
tagListRequestInput := extras.NewExtrasTagsListParams().WithName(&tagName)

mockExtras.EXPECT().ExtrasTagsList(tagListRequestInput, nil).Return(nil, errors.New("some error"))
netboxClient := &NetboxClient{Extras: mockExtras}

actual, err := netboxClient.GetTagDetails(tagName, "")
assert.Nil(t, actual)
assert.Contains(t, err.Error(), "failed to fetch Tag details")
}

func TestTags_GetTagDetailsNoInput(t *testing.T) {
netboxClient := &NetboxClient{}
actual, err := netboxClient.GetTagDetails("", "")
assert.Nil(t, actual)
assert.Contains(t, err.Error(), "either name or slug must be provided")
}
1 change: 1 addition & 0 deletions pkg/netbox/interfaces/netbox.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ type TenancyInterface interface {

type ExtrasInterface interface {
ExtrasCustomFieldsList(params *extras.ExtrasCustomFieldsListParams, authInfo runtime.ClientAuthInfoWriter, opts ...extras.ClientOption) (*extras.ExtrasCustomFieldsListOK, error)
ExtrasTagsList(params *extras.ExtrasTagsListParams, authInfo runtime.ClientAuthInfoWriter, opts ...extras.ClientOption) (*extras.ExtrasTagsListOK, error)
}

type DcimInterface interface {
Expand Down
7 changes: 7 additions & 0 deletions pkg/netbox/models/ipam.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ type Tenant struct {
Slug string `json:"slug,omitempty"`
}

type Tag struct {
Id int64 `json:"id,omitempty"`
Name string `json:"name,omitempty"`
Slug string `json:"slug,omitempty"`
}

type Site struct {
Id int64 `json:"id,omitempty"`
Name string `json:"name,omitempty"`
Expand All @@ -35,6 +41,7 @@ type NetboxMetadata struct {
Region string `json:"region,omitempty"`
Site string `json:"site,omitempty"`
Tenant string `json:"tenant,omitempty"`
Tags []Tag `json:"tags,omitempty"`
}

type IPAddress struct {
Expand Down