Skip to content

Commit

Permalink
Add namespace proto and registration (#18848)
Browse files Browse the repository at this point in the history
* add namespace proto and registration

* fix proto generation

* add missing copywrite headers

* fix proto linter errors

* fix exports and Type export

* add mutate hook and more validation

* add more validation rules and tests

* Apply suggestions from code review

Co-authored-by: Semir Patel <[email protected]>

* fix owner error and add test

* remove ACL for now

* add tests around space suffix prefix.

* only fait when ns and ap are default, add test for it

---------

Co-authored-by: Semir Patel <[email protected]>
  • Loading branch information
dhiaayachi and analogue authored Sep 20, 2023
1 parent 9e3794e commit 341dc28
Show file tree
Hide file tree
Showing 9 changed files with 468 additions and 0 deletions.
2 changes: 2 additions & 0 deletions agent/consul/type_registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/hashicorp/consul/internal/mesh"
"github.com/hashicorp/consul/internal/resource"
"github.com/hashicorp/consul/internal/resource/demo"
"github.com/hashicorp/consul/internal/tenancy"
)

// NewTypeRegistry returns a registry populated with all supported resource
Expand All @@ -25,6 +26,7 @@ func NewTypeRegistry() resource.Registry {
mesh.RegisterTypes(registry)
catalog.RegisterTypes(registry)
auth.RegisterTypes(registry)
tenancy.RegisterTypes(registry)

return registry
}
28 changes: 28 additions & 0 deletions internal/tenancy/exports.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package tenancy

import (
"github.com/hashicorp/consul/internal/resource"
"github.com/hashicorp/consul/internal/tenancy/internal/types"
)

var (
// API Group Information

APIGroup = types.GroupName
VersionV1Alpha1 = types.VersionV1Alpha1
CurrentVersion = types.CurrentVersion

// Resource Kind Names.

NamespaceKind = types.NamespaceKind
NamespaceV1Alpha1Type = types.NamespaceV1Alpha1Type
)

// RegisterTypes adds all resource types within the "tenancy" API group
// to the given type registry
func RegisterTypes(r resource.Registry) {
types.Register(r)
}
11 changes: 11 additions & 0 deletions internal/tenancy/internal/types/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package types

import "errors"

var (
errInvalidName = errors.New("invalid namespace name provided")
errOwnerNonEmpty = errors.New("namespace should not have an owner")
)
69 changes: 69 additions & 0 deletions internal/tenancy/internal/types/namespace.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package types

import (
"fmt"
"github.com/hashicorp/consul/agent/dns"
"github.com/hashicorp/consul/internal/resource"
"github.com/hashicorp/consul/proto-public/pbresource"
tenancyv1alpha1 "github.com/hashicorp/consul/proto-public/pbtenancy/v1alpha1"
"strings"
)

const (
NamespaceKind = "Namespace"
)

var (
NamespaceV1Alpha1Type = &pbresource.Type{
Group: GroupName,
GroupVersion: VersionV1Alpha1,
Kind: NamespaceKind,
}
NamespaceType = NamespaceV1Alpha1Type
)

func RegisterNamespace(r resource.Registry) {
r.Register(resource.Registration{
Type: NamespaceV1Alpha1Type,
Proto: &tenancyv1alpha1.Namespace{},
Scope: resource.ScopePartition,
Validate: ValidateNamespace,
Mutate: MutateNamespace,
})
}

func MutateNamespace(res *pbresource.Resource) error {
res.Id.Name = strings.ToLower(res.Id.Name)
return nil
}

func ValidateNamespace(res *pbresource.Resource) error {
var ns tenancyv1alpha1.Namespace

if err := res.Data.UnmarshalTo(&ns); err != nil {
return resource.NewErrDataParse(&ns, err)
}
if res.Owner != nil {
return errOwnerNonEmpty
}

// it's not allowed to create default/default tenancy
if res.Id.Name == resource.DefaultNamespaceName && res.Id.Tenancy.Partition == resource.DefaultPartitionName {
return errInvalidName
}

if !dns.IsValidLabel(res.Id.Name) {
return fmt.Errorf("namespace name %q is not a valid DNS hostname", res.Id.Name)
}

switch strings.ToLower(res.Id.Name) {
case "system", "universal", "operator", "root":
return fmt.Errorf("namespace %q is reserved for future internal use", res.Id.Name)
default:

return nil
}
}
142 changes: 142 additions & 0 deletions internal/tenancy/internal/types/namespace_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package types

import (
"errors"
"github.com/hashicorp/consul/internal/resource"
pbcatalog "github.com/hashicorp/consul/proto-public/pbcatalog/v1alpha1"
"github.com/hashicorp/consul/proto-public/pbresource"
tenancyv1alpha1 "github.com/hashicorp/consul/proto-public/pbtenancy/v1alpha1"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/types/known/anypb"
"testing"
)

func createNamespaceResource(t *testing.T, data protoreflect.ProtoMessage) *pbresource.Resource {
res := &pbresource.Resource{
Id: &pbresource.ID{
Type: NamespaceV1Alpha1Type,
Tenancy: resource.DefaultPartitionedTenancy(),
Name: "ns1234",
},
}

var err error
res.Data, err = anypb.New(data)
require.NoError(t, err)
return res
}

func validNamespace() *tenancyv1alpha1.Namespace {
return &tenancyv1alpha1.Namespace{
Description: "description from user",
}
}

func TestValidateNamespace_Ok(t *testing.T) {
res := createNamespaceResource(t, validNamespace())

err := ValidateNamespace(res)
require.NoError(t, err)
}

func TestValidateNamespace_defaultNamespace(t *testing.T) {
res := createNamespaceResource(t, validNamespace())
res.Id.Name = resource.DefaultNamespaceName

err := ValidateNamespace(res)
require.Error(t, err)
require.ErrorAs(t, err, &errInvalidName)
}

func TestValidateNamespace_defaultNamespaceNonDefaultPartition(t *testing.T) {
res := createNamespaceResource(t, validNamespace())
res.Id.Name = resource.DefaultNamespaceName
res.Id.Tenancy.Partition = "foo"

err := ValidateNamespace(res)
require.NoError(t, err)
}

func TestValidateNamespace_InvalidName(t *testing.T) {
res := createNamespaceResource(t, validNamespace())
res.Id.Name = "-invalid"

err := ValidateNamespace(res)
require.Error(t, err)
require.ErrorAs(t, err, &errInvalidName)
}

func TestValidateNamespace_InvalidOwner(t *testing.T) {
res := createNamespaceResource(t, validNamespace())
res.Owner = &pbresource.ID{}
err := ValidateNamespace(res)

require.Error(t, err)
require.ErrorAs(t, err, &errOwnerNonEmpty)
}

func TestValidateNamespace_ParseError(t *testing.T) {
// Any type other than the Namespace type would work
// to cause the error we are expecting
data := &pbcatalog.IP{Address: "198.18.0.1"}

res := createNamespaceResource(t, data)

err := ValidateNamespace(res)
require.Error(t, err)
require.ErrorAs(t, err, &resource.ErrDataParse{})
}

func TestMutateNamespace(t *testing.T) {
tests := []struct {
name string
namespaceName string
expectedName string
err error
}{
{"lower", "lower", "lower", nil},
{"mixed", "MiXeD", "mixed", nil},
{"upper", "UPPER", "upper", nil},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
res := &pbresource.Resource{Id: &pbresource.ID{Name: tt.namespaceName}}
if err := MutateNamespace(res); !errors.Is(err, tt.err) {
t.Errorf("MutateNamespace() error = %v", err)
}
require.Equal(t, res.Id.Name, tt.expectedName)
})
}
}

func TestValidateNamespace(t *testing.T) {
tests := []struct {
name string
namespaceName string
err string
}{
{"system", "System", "namespace \"System\" is reserved for future internal use"},
{"invalid", "-inval", "namespace name \"-inval\" is not a valid DNS hostname"},
{"valid", "ns1", ""},
{"space prefix", " foo", "namespace name \" foo\" is not a valid DNS hostname"},
{"space suffix", "bar ", "namespace name \"bar \" is not a valid DNS hostname"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a, err := anypb.New(&tenancyv1alpha1.Namespace{})
require.NoError(t, err)
res := &pbresource.Resource{Id: &pbresource.ID{Name: tt.namespaceName}, Data: a}
err = ValidateNamespace(res)
if tt.err == "" {
require.NoError(t, err)
} else {
require.Equal(t, err.Error(), tt.err)
}

})
}
}
18 changes: 18 additions & 0 deletions internal/tenancy/internal/types/types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package types

import (
"github.com/hashicorp/consul/internal/resource"
)

const (
GroupName = "tenancy"
VersionV1Alpha1 = "v1alpha1"
CurrentVersion = VersionV1Alpha1
)

func Register(r resource.Registry) {
RegisterNamespace(r)
}
18 changes: 18 additions & 0 deletions proto-public/pbtenancy/v1alpha1/namespace.pb.binary.go

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

Loading

0 comments on commit 341dc28

Please sign in to comment.