From 341dc28ff9d34a05789d89c5cc8f480e23534b5e Mon Sep 17 00:00:00 2001 From: Dhia Ayachi Date: Wed, 20 Sep 2023 15:20:20 -0400 Subject: [PATCH] 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 * 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 --- agent/consul/type_registry.go | 2 + internal/tenancy/exports.go | 28 +++ internal/tenancy/internal/types/errors.go | 11 ++ internal/tenancy/internal/types/namespace.go | 69 +++++++ .../tenancy/internal/types/namespace_test.go | 142 +++++++++++++++ internal/tenancy/internal/types/types.go | 18 ++ .../pbtenancy/v1alpha1/namespace.pb.binary.go | 18 ++ .../pbtenancy/v1alpha1/namespace.pb.go | 168 ++++++++++++++++++ .../pbtenancy/v1alpha1/namespace.proto | 12 ++ 9 files changed, 468 insertions(+) create mode 100644 internal/tenancy/exports.go create mode 100644 internal/tenancy/internal/types/errors.go create mode 100644 internal/tenancy/internal/types/namespace.go create mode 100644 internal/tenancy/internal/types/namespace_test.go create mode 100644 internal/tenancy/internal/types/types.go create mode 100644 proto-public/pbtenancy/v1alpha1/namespace.pb.binary.go create mode 100644 proto-public/pbtenancy/v1alpha1/namespace.pb.go create mode 100644 proto-public/pbtenancy/v1alpha1/namespace.proto diff --git a/agent/consul/type_registry.go b/agent/consul/type_registry.go index a23f72b9b2..d93309159d 100644 --- a/agent/consul/type_registry.go +++ b/agent/consul/type_registry.go @@ -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 } diff --git a/internal/tenancy/exports.go b/internal/tenancy/exports.go new file mode 100644 index 0000000000..aadd7efb59 --- /dev/null +++ b/internal/tenancy/exports.go @@ -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) +} diff --git a/internal/tenancy/internal/types/errors.go b/internal/tenancy/internal/types/errors.go new file mode 100644 index 0000000000..42df9b319f --- /dev/null +++ b/internal/tenancy/internal/types/errors.go @@ -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") +) diff --git a/internal/tenancy/internal/types/namespace.go b/internal/tenancy/internal/types/namespace.go new file mode 100644 index 0000000000..ac9848a4f2 --- /dev/null +++ b/internal/tenancy/internal/types/namespace.go @@ -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 + } +} diff --git a/internal/tenancy/internal/types/namespace_test.go b/internal/tenancy/internal/types/namespace_test.go new file mode 100644 index 0000000000..7f79437c5e --- /dev/null +++ b/internal/tenancy/internal/types/namespace_test.go @@ -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) + } + + }) + } +} diff --git a/internal/tenancy/internal/types/types.go b/internal/tenancy/internal/types/types.go new file mode 100644 index 0000000000..ab6fd8d60f --- /dev/null +++ b/internal/tenancy/internal/types/types.go @@ -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) +} diff --git a/proto-public/pbtenancy/v1alpha1/namespace.pb.binary.go b/proto-public/pbtenancy/v1alpha1/namespace.pb.binary.go new file mode 100644 index 0000000000..f6097062d3 --- /dev/null +++ b/proto-public/pbtenancy/v1alpha1/namespace.pb.binary.go @@ -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) +} diff --git a/proto-public/pbtenancy/v1alpha1/namespace.pb.go b/proto-public/pbtenancy/v1alpha1/namespace.pb.go new file mode 100644 index 0000000000..e5993ab0af --- /dev/null +++ b/proto-public/pbtenancy/v1alpha1/namespace.pb.go @@ -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 +} diff --git a/proto-public/pbtenancy/v1alpha1/namespace.proto b/proto-public/pbtenancy/v1alpha1/namespace.proto new file mode 100644 index 0000000000..62a5ceb038 --- /dev/null +++ b/proto-public/pbtenancy/v1alpha1/namespace.proto @@ -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; +}