Skip to content

Commit 64d3d0d

Browse files
authored
Implement human-readable random identifier generation (#653)
* Allow human-readable identifier generation * Apply suggestions from code review * Use pet names for random identifiers * fixup! Allow human-readable identifier generation * fixup! Use pet names for random identifiers
1 parent 66e18e4 commit 64d3d0d

File tree

8 files changed

+61
-38
lines changed

8 files changed

+61
-38
lines changed

cmd/create/create.go

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -102,13 +102,9 @@ func (o *Options) Run(cmd *cobra.Command, args []string, cloud *common.Cloud) er
102102
cfg.Spot = common.Spot(common.SpotEnabled)
103103
}
104104

105-
id := common.NewRandomIdentifier()
106-
107-
if o.Name != "" {
108-
id = common.NewIdentifier(o.Name)
109-
if identifier, err := common.ParseIdentifier(o.Name); err == nil {
110-
id = identifier
111-
}
105+
id := common.NewRandomIdentifier(o.Name)
106+
if identifier, err := common.ParseIdentifier(o.Name); err == nil {
107+
id = identifier
112108
}
113109

114110
ctx, cancel := context.WithTimeout(context.Background(), cloud.Timeouts.Create)

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ require (
2323
github.com/brianvoe/gofakeit/v6 v6.9.0
2424
github.com/cloudflare/gokey v0.1.0
2525
github.com/docker/go-units v0.4.0
26+
github.com/dustinkirkland/golang-petname v0.0.0-20191129215211-8e5a1ed0cff0 // indirect
2627
github.com/gobwas/glob v0.2.3
2728
github.com/google/go-github/v42 v42.0.0
2829
github.com/google/go-github/v45 v45.2.0

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,8 @@ github.com/dropbox/dropbox-sdk-go-unofficial/v6 v6.0.3 h1:h71/Ky9+298V45NSkxjhFv
325325
github.com/dropbox/dropbox-sdk-go-unofficial/v6 v6.0.3/go.mod h1:rSS3kM9XMzSQ6pw91Qgd6yB5jdt70N4OdtrAf74As5M=
326326
github.com/dustin/go-humanize v0.0.0-20180421182945-02af3965c54e/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
327327
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
328+
github.com/dustinkirkland/golang-petname v0.0.0-20191129215211-8e5a1ed0cff0 h1:90Ly+6UfUypEF6vvvW5rQIv9opIL8CbmW9FT20LDQoY=
329+
github.com/dustinkirkland/golang-petname v0.0.0-20191129215211-8e5a1ed0cff0/go.mod h1:V+Qd57rJe8gd4eiGzZyg4h54VLHmYVVw54iMnlAMrF8=
328330
github.com/dvyukov/go-fuzz v0.0.0-20200318091601-be3528f3a813/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
329331
github.com/elazarl/go-bindata-assetfs v1.0.0/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4=
330332
github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153 h1:yUdfgN0XgIJw7foRItutHYUIhlcKzcSf5vDpdhQAKTc=

iterative/resource_task.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -374,16 +374,16 @@ func resourceTaskBuild(ctx context.Context, d *schema.ResourceData, m interface{
374374
if newId, err := common.ParseIdentifier(name); err == nil {
375375
id = newId
376376
} else {
377-
id = common.NewIdentifier(name)
377+
id = common.NewDeterministicIdentifier(name)
378378
}
379379
} else if name := os.Getenv("GITHUB_RUN_ID"); name != "" {
380-
id = common.NewIdentifier(name)
380+
id = common.NewDeterministicIdentifier(name)
381381
} else if name := os.Getenv("CI_PIPELINE_ID"); name != "" {
382-
id = common.NewIdentifier(name)
382+
id = common.NewDeterministicIdentifier(name)
383383
} else if name := os.Getenv("BITBUCKET_STEP_TRIGGERER_UUID"); name != "" {
384-
id = common.NewIdentifier(name)
384+
id = common.NewDeterministicIdentifier(name)
385385
} else {
386-
id = common.NewRandomIdentifier()
386+
id = common.NewRandomIdentifier("")
387387
}
388388
}
389389

task/common/identifier.go

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,40 +10,56 @@ import (
1010
"crypto/sha256"
1111

1212
"github.com/aohorodnyk/uid"
13+
"github.com/dustinkirkland/golang-petname"
1314
)
1415

15-
type Identifier string
16+
type Identifier struct {
17+
name string
18+
salt string
19+
}
1620

1721
var ErrWrongIdentifier = errors.New("wrong identifier")
1822

1923
const (
2024
maximumLongLength = 50
2125
shortLength = 16
26+
nameLength = maximumLongLength-shortLength-uint32(len("tpi---"))
2227
)
2328

2429
func ParseIdentifier(identifier string) (Identifier, error) {
2530
re := regexp.MustCompile(`(?s)^tpi-([a-z0-9]+(?:[a-z0-9-]*[a-z0-9])?)-([a-z0-9]+)-([a-z0-9]+)$`)
2631

2732
if match := re.FindStringSubmatch(string(identifier)); len(match) > 0 && hash(match[1]+match[2], shortLength/2) == match[3] {
28-
return Identifier(match[1]), nil
33+
return Identifier{name: match[1], salt: match[2]}, nil
2934
}
3035

31-
return Identifier(""), ErrWrongIdentifier
36+
return Identifier{}, ErrWrongIdentifier
3237
}
3338

34-
func NewIdentifier(identifier string) Identifier {
35-
return Identifier(identifier)
39+
// NewDeterministicIdentifier returns a new deterministic Identifier, using the
40+
// provided name as a seed. Repeated calls to this function are guaranteed to
41+
// generate the same Identifier.
42+
func NewDeterministicIdentifier(name string) Identifier {
43+
seed := normalize(name, nameLength)
44+
return Identifier{name: name, salt: hash(seed, shortLength/2)}
3645
}
3746

38-
func NewRandomIdentifier() Identifier {
39-
return NewIdentifier(uid.NewProvider36Size(8).MustGenerate().String())
47+
// NewRandomIdentifier returns a new random Identifier. Repeated calls to this
48+
// function are guaranteed to generate different Identifiers, as long as there
49+
// are no collisions.
50+
func NewRandomIdentifier(name string) Identifier {
51+
seed := uid.NewProvider36Size(8).MustGenerate().String()
52+
if name == "" {
53+
petname.NonDeterministicMode()
54+
name = petname.Generate(3, "-")
55+
}
56+
57+
return Identifier{name: name, salt: hash(seed, shortLength/2)}
4058
}
4159

4260
func (i Identifier) Long() string {
43-
name := normalize(string(i), maximumLongLength-shortLength-uint32(len("tpi---")))
44-
digest := hash(name, shortLength/2)
45-
46-
return fmt.Sprintf("tpi-%s-%s-%s", name, digest, hash(name+digest, shortLength/2))
61+
name := normalize(i.name, nameLength)
62+
return fmt.Sprintf("tpi-%s-%s-%s", name, i.salt, hash(name+i.salt, shortLength/2))
4763
}
4864

4965
func (i Identifier) Short() string {
@@ -52,9 +68,9 @@ func (i Identifier) Short() string {
5268
}
5369

5470
// hash deterministically generates a Base36 digest of `size`
55-
// characters using `identifier` as the seed.
56-
func hash(identifier string, size uint8) string {
57-
digest := sha256.Sum256([]byte(identifier))
71+
// characters using the provided seed.
72+
func hash(seed string, size uint8) string {
73+
digest := sha256.Sum256([]byte(seed))
5874
random := uid.NewRandCustom(bytes.NewReader(digest[:]))
5975
encoder := uid.NewEncoderBase36()
6076
provider := uid.NewProviderCustom(sha256.Size, random, encoder)

task/common/identifier_test.go

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,14 @@ import (
1010
func TestIdentifier(t *testing.T) {
1111
name := gofakeit.NewCrypto().Sentence(512)
1212
t.Run("stability", func(t *testing.T) {
13-
identifier := NewIdentifier(name)
13+
identifier := NewDeterministicIdentifier(name)
1414

1515
require.Equal(t, identifier.Long(), identifier.Long())
1616
require.Equal(t, identifier.Short(), identifier.Short())
1717
})
1818

19-
t.Run("consistent", func(t *testing.T) {
20-
identifier := NewIdentifier("5299fe10-79e9-4c3b-b15e-036e8e60ab6c")
19+
t.Run("consistency", func(t *testing.T) {
20+
identifier := NewDeterministicIdentifier("5299fe10-79e9-4c3b-b15e-036e8e60ab6c")
2121
parsed, err := ParseIdentifier(identifier.Long())
2222

2323
require.NoError(t, err)
@@ -26,7 +26,7 @@ func TestIdentifier(t *testing.T) {
2626
})
2727

2828
t.Run("homogeneity", func(t *testing.T) {
29-
identifier := NewIdentifier(name)
29+
identifier := NewDeterministicIdentifier(name)
3030

3131
long := identifier.Long()
3232
short := identifier.Short()
@@ -39,13 +39,26 @@ func TestIdentifier(t *testing.T) {
3939
})
4040

4141
t.Run("compatibility", func(t *testing.T) {
42-
identifier := NewIdentifier("test")
42+
identifier := NewDeterministicIdentifier("test")
4343

4444
require.Equal(t, "tpi-test-3z4xlzwq-3u0vweb4", identifier.Long())
4545
require.Equal(t, "3z4xlzwq3u0vweb4", identifier.Short())
4646

4747
parsed, err := ParseIdentifier(identifier.Long())
48-
require.Equal(t, parsed, identifier)
48+
require.Equal(t, parsed.Long(), identifier.Long())
4949
require.NoError(t, err)
5050
})
51+
52+
t.Run("randomness", func(t *testing.T) {
53+
name := "test"
54+
55+
first := NewRandomIdentifier(name)
56+
second := NewRandomIdentifier(name)
57+
58+
require.NotEqual(t, first.Long(), second.Long())
59+
require.NotEqual(t, first.Short(), second.Short())
60+
61+
require.Contains(t, first.Long(), name)
62+
require.Contains(t, second.Long(), name)
63+
})
5164
}

task/task.go

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package task
22

33
import (
44
"context"
5-
"errors"
65
"fmt"
76
"net"
87

@@ -31,10 +30,6 @@ func List(ctx context.Context, cloud common.Cloud) ([]common.Identifier, error)
3130
}
3231

3332
func New(ctx context.Context, cloud common.Cloud, identifier common.Identifier, task common.Task) (Task, error) {
34-
if identifier == "" {
35-
return nil, errors.New("identifier must not be empty")
36-
}
37-
3833
switch cloud.Provider {
3934
case common.ProviderAWS:
4035
return aws.New(ctx, cloud, identifier, task)

task/task_smoke_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ func TestTaskSmoke(t *testing.T) {
7272
},
7373
}
7474

75-
identifier := common.NewIdentifier(testName)
75+
identifier := common.NewDeterministicIdentifier(testName)
7676

7777
task := common.Task{
7878
Size: common.Size{

0 commit comments

Comments
 (0)