Add namespace proto and registration (#18848)

* 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 <semir.patel@hashicorp.com>

* 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 <semir.patel@hashicorp.com>
This commit is contained in:
Dhia Ayachi 2023-09-20 15:20:20 -04:00 committed by GitHub
parent 9e3794ee48
commit 341dc28ff9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 468 additions and 0 deletions

View File

@ -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
@ -25,6 +26,7 @@ func NewTypeRegistry() resource.Registry {
mesh.RegisterTypes(registry)
catalog.RegisterTypes(registry)
auth.RegisterTypes(registry)
tenancy.RegisterTypes(registry)
return registry
}

View File

@ -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)
}

View File

@ -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")
)

View File

@ -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
}
}

View File

@ -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)
}
})
}
}

View File

@ -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)
}

View File

@ -0,0 +1,18 @@
// Code generated by protoc-gen-go-binary. DO NOT EDIT.
// source: pbtenancy/v1alpha1/namespace.proto
package tenancyv1alpha1
import (
"google.golang.org/protobuf/proto"
)
// MarshalBinary implements encoding.BinaryMarshaler
func (msg *Namespace) MarshalBinary() ([]byte, error) {
return proto.Marshal(msg)
}
// UnmarshalBinary implements encoding.BinaryUnmarshaler
func (msg *Namespace) UnmarshalBinary(b []byte) error {
return proto.Unmarshal(b, msg)
}

View File

@ -0,0 +1,168 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.30.0
// protoc (unknown)
// source: pbtenancy/v1alpha1/namespace.proto
package tenancyv1alpha1
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
// The name of the Namespace is in the outer Resource.ID.Name.
// It must be unique within a partition and must be a
// DNS hostname. There are also other reserved names that may not be used.
type Namespace struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// Description is where the user puts any information they want
// about the namespace. It is not used internally.
Description string `protobuf:"bytes,1,opt,name=description,proto3" json:"description,omitempty"`
}
func (x *Namespace) Reset() {
*x = Namespace{}
if protoimpl.UnsafeEnabled {
mi := &file_pbtenancy_v1alpha1_namespace_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Namespace) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Namespace) ProtoMessage() {}
func (x *Namespace) ProtoReflect() protoreflect.Message {
mi := &file_pbtenancy_v1alpha1_namespace_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Namespace.ProtoReflect.Descriptor instead.
func (*Namespace) Descriptor() ([]byte, []int) {
return file_pbtenancy_v1alpha1_namespace_proto_rawDescGZIP(), []int{0}
}
func (x *Namespace) GetDescription() string {
if x != nil {
return x.Description
}
return ""
}
var File_pbtenancy_v1alpha1_namespace_proto protoreflect.FileDescriptor
var file_pbtenancy_v1alpha1_namespace_proto_rawDesc = []byte{
0x0a, 0x22, 0x70, 0x62, 0x74, 0x65, 0x6e, 0x61, 0x6e, 0x63, 0x79, 0x2f, 0x76, 0x31, 0x61, 0x6c,
0x70, 0x68, 0x61, 0x31, 0x2f, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x2e, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x12, 0x21, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e,
0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x74, 0x65, 0x6e, 0x61, 0x6e, 0x63, 0x79, 0x2e, 0x76,
0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x22, 0x2d, 0x0a, 0x09, 0x4e, 0x61, 0x6d, 0x65, 0x73,
0x70, 0x61, 0x63, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74,
0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72,
0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0xab, 0x02, 0x0a, 0x25, 0x63, 0x6f, 0x6d, 0x2e, 0x68,
0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e,
0x74, 0x65, 0x6e, 0x61, 0x6e, 0x63, 0x79, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31,
0x42, 0x0e, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f,
0x50, 0x01, 0x5a, 0x4b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68,
0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2f,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2d, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x2f, 0x70, 0x62, 0x74,
0x65, 0x6e, 0x61, 0x6e, 0x63, 0x79, 0x2f, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x3b,
0x74, 0x65, 0x6e, 0x61, 0x6e, 0x63, 0x79, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0xa2,
0x02, 0x03, 0x48, 0x43, 0x54, 0xaa, 0x02, 0x21, 0x48, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72,
0x70, 0x2e, 0x43, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x54, 0x65, 0x6e, 0x61, 0x6e, 0x63, 0x79,
0x2e, 0x56, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0xca, 0x02, 0x21, 0x48, 0x61, 0x73, 0x68,
0x69, 0x63, 0x6f, 0x72, 0x70, 0x5c, 0x43, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x5c, 0x54, 0x65, 0x6e,
0x61, 0x6e, 0x63, 0x79, 0x5c, 0x56, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0xe2, 0x02, 0x2d,
0x48, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x5c, 0x43, 0x6f, 0x6e, 0x73, 0x75, 0x6c,
0x5c, 0x54, 0x65, 0x6e, 0x61, 0x6e, 0x63, 0x79, 0x5c, 0x56, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61,
0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x24,
0x48, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x3a, 0x3a, 0x43, 0x6f, 0x6e, 0x73, 0x75,
0x6c, 0x3a, 0x3a, 0x54, 0x65, 0x6e, 0x61, 0x6e, 0x63, 0x79, 0x3a, 0x3a, 0x56, 0x31, 0x61, 0x6c,
0x70, 0x68, 0x61, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
file_pbtenancy_v1alpha1_namespace_proto_rawDescOnce sync.Once
file_pbtenancy_v1alpha1_namespace_proto_rawDescData = file_pbtenancy_v1alpha1_namespace_proto_rawDesc
)
func file_pbtenancy_v1alpha1_namespace_proto_rawDescGZIP() []byte {
file_pbtenancy_v1alpha1_namespace_proto_rawDescOnce.Do(func() {
file_pbtenancy_v1alpha1_namespace_proto_rawDescData = protoimpl.X.CompressGZIP(file_pbtenancy_v1alpha1_namespace_proto_rawDescData)
})
return file_pbtenancy_v1alpha1_namespace_proto_rawDescData
}
var file_pbtenancy_v1alpha1_namespace_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
var file_pbtenancy_v1alpha1_namespace_proto_goTypes = []interface{}{
(*Namespace)(nil), // 0: hashicorp.consul.tenancy.v1alpha1.Namespace
}
var file_pbtenancy_v1alpha1_namespace_proto_depIdxs = []int32{
0, // [0:0] is the sub-list for method output_type
0, // [0:0] is the sub-list for method input_type
0, // [0:0] is the sub-list for extension type_name
0, // [0:0] is the sub-list for extension extendee
0, // [0:0] is the sub-list for field type_name
}
func init() { file_pbtenancy_v1alpha1_namespace_proto_init() }
func file_pbtenancy_v1alpha1_namespace_proto_init() {
if File_pbtenancy_v1alpha1_namespace_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_pbtenancy_v1alpha1_namespace_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Namespace); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_pbtenancy_v1alpha1_namespace_proto_rawDesc,
NumEnums: 0,
NumMessages: 1,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_pbtenancy_v1alpha1_namespace_proto_goTypes,
DependencyIndexes: file_pbtenancy_v1alpha1_namespace_proto_depIdxs,
MessageInfos: file_pbtenancy_v1alpha1_namespace_proto_msgTypes,
}.Build()
File_pbtenancy_v1alpha1_namespace_proto = out.File
file_pbtenancy_v1alpha1_namespace_proto_rawDesc = nil
file_pbtenancy_v1alpha1_namespace_proto_goTypes = nil
file_pbtenancy_v1alpha1_namespace_proto_depIdxs = nil
}

View File

@ -0,0 +1,12 @@
syntax = "proto3";
package hashicorp.consul.tenancy.v1alpha1;
// The name of the Namespace is in the outer Resource.ID.Name.
// It must be unique within a partition and must be a
// DNS hostname. There are also other reserved names that may not be used.
message Namespace {
// Description is where the user puts any information they want
// about the namespace. It is not used internally.
string description = 1;
}