Merge pull request 'feat: Instance Identity 与 Revision 值对象及测试' (#9) from feature/instance-identity-values into main
Reviewed-on: #9
This commit was merged in pull request #9.
This commit is contained in:
@@ -36,8 +36,8 @@ type CredentialReference struct {
|
|||||||
values CredentialReferenceValues
|
values CredentialReferenceValues
|
||||||
}
|
}
|
||||||
|
|
||||||
// Secret names follow the Kubernetes DNS subdomain syntax and 253-character limit.
|
// Instance and Secret names share the DNS subdomain syntax and 253-character limit.
|
||||||
var secretName = regexp.MustCompile(`^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$`)
|
var dnsSubdomainName = regexp.MustCompile(`^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$`)
|
||||||
|
|
||||||
func NewCredentialReference(values CredentialReferenceValues) (CredentialReference, error) {
|
func NewCredentialReference(values CredentialReferenceValues) (CredentialReference, error) {
|
||||||
reference := CredentialReference{values: values}
|
reference := CredentialReference{values: values}
|
||||||
@@ -54,7 +54,7 @@ func (r CredentialReference) Values() CredentialReferenceValues { return r.value
|
|||||||
// Checking that the referenced Secret contains nonempty credentials is an application
|
// Checking that the referenced Secret contains nonempty credentials is an application
|
||||||
// responsibility. Errors omit input values and no implicit defaults are applied.
|
// responsibility. Errors omit input values and no implicit defaults are applied.
|
||||||
func (r CredentialReference) Validate() error {
|
func (r CredentialReference) Validate() error {
|
||||||
if len(r.values.Name) > 253 || !secretName.MatchString(r.values.Name) {
|
if len(r.values.Name) > 253 || !dnsSubdomainName.MatchString(r.values.Name) {
|
||||||
return errors.New("management Secret name must be a valid DNS subdomain of at most 253 characters")
|
return errors.New("management Secret name must be a valid DNS subdomain of at most 253 characters")
|
||||||
}
|
}
|
||||||
if r.values.UsernameKey == "" {
|
if r.values.UsernameKey == "" {
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2026.
|
||||||
|
|
||||||
|
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 instance
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
// Identity identifies one registration, not a physical PostgreSQL server.
|
||||||
|
// UID is opaque; a recreated resource with the same name has a different identity.
|
||||||
|
// Its zero value is invalid and must be rejected when constructing an aggregate.
|
||||||
|
type Identity struct {
|
||||||
|
uid string
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewIdentity(uid, name string) (Identity, error) {
|
||||||
|
identity := Identity{uid: uid, name: name}
|
||||||
|
if err := identity.Validate(); err != nil {
|
||||||
|
return Identity{}, err
|
||||||
|
}
|
||||||
|
return identity, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i Identity) UID() string { return i.uid }
|
||||||
|
func (i Identity) Name() string { return i.name }
|
||||||
|
|
||||||
|
// Validate checks registration values without looking up any external identity.
|
||||||
|
func (i Identity) Validate() error {
|
||||||
|
if i.uid == "" {
|
||||||
|
return errors.New("instance UID is required")
|
||||||
|
}
|
||||||
|
if len(i.name) > 253 || !dnsSubdomainName.MatchString(i.name) {
|
||||||
|
return errors.New("instance name must be a valid DNS subdomain of at most 253 characters")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Revision is a positive configuration generation, separate from Identity.
|
||||||
|
// Zero is invalid for desired configuration; an unobserved status generation of
|
||||||
|
// zero must be represented separately when the aggregate is implemented.
|
||||||
|
type Revision struct {
|
||||||
|
value int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRevision(value int64) (Revision, error) {
|
||||||
|
revision := Revision{value: value}
|
||||||
|
if err := revision.Validate(); err != nil {
|
||||||
|
return Revision{}, err
|
||||||
|
}
|
||||||
|
return revision, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r Revision) Value() int64 { return r.value }
|
||||||
|
|
||||||
|
func (r Revision) Validate() error {
|
||||||
|
if r.value <= 0 {
|
||||||
|
return errors.New("instance revision must be positive")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2026.
|
||||||
|
|
||||||
|
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 instance_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.ddupan.top/panxiao81/postgresql-tenant-operator/internal/domain/instance"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Acceptance: docs/domain-instance.md §2: registration identity is UID + name,
|
||||||
|
// independent of the configuration revision or physical PostgreSQL server.
|
||||||
|
func TestIdentityDistinguishesRecreatedInstances(t *testing.T) {
|
||||||
|
first, err := instance.NewIdentity("opaque-uid-1", "shared")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
same, err := instance.NewIdentity("opaque-uid-1", "shared")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
recreated, err := instance.NewIdentity("opaque-uid-2", "shared")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if first != same || first == recreated {
|
||||||
|
t.Fatal("identity must distinguish same-name registrations by UID")
|
||||||
|
}
|
||||||
|
if first.UID() != "opaque-uid-1" || first.Name() != "shared" {
|
||||||
|
t.Fatal("identity changed declared values")
|
||||||
|
}
|
||||||
|
if err := first.Validate(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIdentityValidation(t *testing.T) {
|
||||||
|
for _, name := range []string{"", "Shared", "shared_name", "ns/shared", "-shared", "pg..shared", strings.Repeat("a", 254)} {
|
||||||
|
identity, err := instance.NewIdentity("uid", name)
|
||||||
|
if err == nil || identity != (instance.Identity{}) {
|
||||||
|
t.Fatal("invalid name accepted or partial identity returned")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, err := instance.NewIdentity("", "shared"); err == nil {
|
||||||
|
t.Fatal("empty UID accepted")
|
||||||
|
}
|
||||||
|
if err := (instance.Identity{}).Validate(); err == nil {
|
||||||
|
t.Fatal("zero identity accepted")
|
||||||
|
}
|
||||||
|
for _, name := range []string{"a", "1", "pg.shared-1", strings.Repeat("a", 253)} {
|
||||||
|
if _, err := instance.NewIdentity("opaque-not-a-uuid", name); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRevisionRequiresPositiveValue(t *testing.T) {
|
||||||
|
for _, value := range []int64{math.MinInt64, -1, 0} {
|
||||||
|
revision, err := instance.NewRevision(value)
|
||||||
|
if err == nil || revision != (instance.Revision{}) {
|
||||||
|
t.Fatal("invalid revision accepted or partial value returned")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, value := range []int64{1, 2, math.MaxInt64} {
|
||||||
|
revision, err := instance.NewRevision(value)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if revision.Value() != value {
|
||||||
|
t.Fatal("revision changed declared value")
|
||||||
|
}
|
||||||
|
if err := revision.Validate(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
same, err := instance.NewRevision(value)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if revision != same {
|
||||||
|
t.Fatal("identical revisions must compare equal")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := (instance.Revision{}).Validate(); err == nil {
|
||||||
|
t.Fatal("zero revision accepted")
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user