Skip to content

Commit 24ebb72

Browse files
authored
Merge pull request hashicorp#10152 from hashicorp/jbardin/unique-id
Fix resource.UniqueId to be properly ordered
2 parents 5108182 + e28e11d commit 24ebb72

2 files changed

Lines changed: 18 additions & 40 deletions

File tree

‎helper/resource/id.go‎

Lines changed: 15 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,48 +2,34 @@ package resource
22

33
import (
44
"crypto/rand"
5-
"encoding/base32"
65
"fmt"
7-
"strings"
8-
"time"
6+
"math/big"
7+
"sync"
98
)
109

1110
const UniqueIdPrefix = `terraform-`
1211

12+
// idCounter is a randomly seeded monotonic counter for generating ordered
13+
// unique ids. It uses a big.Int so we can easily increment a long numeric
14+
// string. The max possible hex value here with 12 random bytes is
15+
// "01000000000000000000000000", so there's no chance of rollover during
16+
// operation.
17+
var idMutex sync.Mutex
18+
var idCounter = big.NewInt(0).SetBytes(randomBytes(12))
19+
1320
// Helper for a resource to generate a unique identifier w/ default prefix
1421
func UniqueId() string {
1522
return PrefixedUniqueId(UniqueIdPrefix)
1623
}
1724

1825
// Helper for a resource to generate a unique identifier w/ given prefix
1926
//
20-
// After the prefix, the ID consists of a timestamp and 12 random base32
21-
// characters. The timestamp means that multiple IDs created with the same
22-
// prefix will sort in the order of their creation.
27+
// After the prefix, the ID consists of an incrementing 26 digit value (to match
28+
// previous timestamp output).
2329
func PrefixedUniqueId(prefix string) string {
24-
// Be precise to the level nanoseconds, but remove the dot before the
25-
// nanosecond. We assume that the randomCharacters call takes at least a
26-
// nanosecond, so that multiple calls to this function from the same goroutine
27-
// will have distinct ordered timestamps.
28-
timestamp := strings.Replace(
29-
time.Now().UTC().Format("20060102150405.000000000"),
30-
".",
31-
"", 1)
32-
// This uses 3 characters, so that the length of the unique ID is the same as
33-
// it was before we added the timestamp prefix, which happened to be 23
34-
// characters.
35-
return fmt.Sprintf("%s%s%s", prefix, timestamp, randomCharacters(3))
36-
}
37-
38-
func randomCharacters(n int) string {
39-
// Base32 has 5 bits of information per character.
40-
b := randomBytes(n * 8 / 5)
41-
chars := strings.ToLower(
42-
strings.Replace(
43-
base32.StdEncoding.EncodeToString(b),
44-
"=", "", -1))
45-
// Trim extra characters.
46-
return chars[:n]
30+
idMutex.Lock()
31+
defer idMutex.Unlock()
32+
return fmt.Sprintf("%s%026x", prefix, idCounter.Add(idCounter, big.NewInt(1)))
4733
}
4834

4935
func randomBytes(n int) []byte {

‎helper/resource/id_test.go‎

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@ import (
66
"testing"
77
)
88

9-
var allDigits = regexp.MustCompile(`^\d+$`)
10-
var allBase32 = regexp.MustCompile(`^[a-z234567]+$`)
9+
var allHex = regexp.MustCompile(`^[a-f0-9]+$`)
1110

1211
func TestUniqueId(t *testing.T) {
1312
iterations := 10000
@@ -30,15 +29,8 @@ func TestUniqueId(t *testing.T) {
3029
t.Fatalf("Post-prefix part has wrong length! %s", rest)
3130
}
3231

33-
timestamp := rest[:23]
34-
random := rest[23:]
35-
36-
if !allDigits.MatchString(timestamp) {
37-
t.Fatalf("Timestamp not all digits! %s", timestamp)
38-
}
39-
40-
if !allBase32.MatchString(random) {
41-
t.Fatalf("Random part not all base32! %s", random)
32+
if !allHex.MatchString(rest) {
33+
t.Fatalf("Random part not all hex! %s", rest)
4234
}
4335

4436
if lastId != "" && lastId >= id {

0 commit comments

Comments
 (0)