Skip to content

Commit 64a31bf

Browse files
author
Allison Pierson
authored
Launch V2: automatically generate valid app name (#2806)
* `launch` v2: check app name is available, attempt to add unique suffix removed herobrine
1 parent c3472a5 commit 64a31bf

File tree

2 files changed

+134
-0
lines changed

2 files changed

+134
-0
lines changed

internal/command/launch/plan_builder.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,12 @@ import (
1515
"github.com/superfly/flyctl/internal/cmdutil"
1616
"github.com/superfly/flyctl/internal/command/launch/plan"
1717
"github.com/superfly/flyctl/internal/flag"
18+
"github.com/superfly/flyctl/internal/flyerr"
19+
"github.com/superfly/flyctl/internal/haikunator"
1820
"github.com/superfly/flyctl/internal/prompt"
1921
"github.com/superfly/flyctl/iostreams"
2022
"github.com/superfly/flyctl/scanner"
23+
"github.com/superfly/graphql"
2124
)
2225

2326
// Cache values between buildManifest and stateFromManifest
@@ -31,6 +34,14 @@ type planBuildCache struct {
3134
srcInfo *scanner.SourceInfo
3235
}
3336

37+
func appNameTakenErr(appName string) error {
38+
return flyerr.GenericErr{
39+
Err: fmt.Sprintf("app name %s is already taken", appName),
40+
Descript: "each Fly.io app must have a unique name",
41+
Suggest: "Please specify a different app name with --name",
42+
}
43+
}
44+
3445
func buildManifest(ctx context.Context) (*LaunchManifest, *planBuildCache, error) {
3546

3647
appConfig, copiedConfig, err := determineBaseAppConfig(ctx)
@@ -183,6 +194,10 @@ func stateFromManifest(ctx context.Context, m LaunchManifest, optionalCache *pla
183194
}
184195
}
185196

197+
if taken, _ := appNameTaken(ctx, appConfig.AppName); taken {
198+
return nil, appNameTakenErr(appConfig.AppName)
199+
}
200+
186201
workingDir := flag.GetString(ctx, "path")
187202
if absDir, err := filepath.Abs(workingDir); err == nil {
188203
workingDir = absDir
@@ -274,9 +289,39 @@ func determineAppName(ctx context.Context, configPath string) (string, string, e
274289
if appName == "" {
275290
return "", "", errors.New("enable to determine app name, please specify one with --name")
276291
}
292+
// If the app name is already taken, try to generate a unique suffix.
293+
if taken, _ := appNameTaken(ctx, appName); taken {
294+
delimiter := "-"
295+
var newName string
296+
found := false
297+
for i := 1; i < 10; i++ {
298+
newName = fmt.Sprintf("%s%s%s", appName, delimiter, haikunator.Haikunator().Delimiter(delimiter))
299+
if taken, _ := appNameTaken(ctx, newName); !taken {
300+
found = true
301+
break
302+
}
303+
}
304+
if !found {
305+
return "", "", appNameTakenErr(appName)
306+
}
307+
appName = newName
308+
}
277309
return appName, "derived from your directory name", nil
278310
}
279311

312+
func appNameTaken(ctx context.Context, name string) (bool, error) {
313+
client := client.FromContext(ctx).API()
314+
_, err := client.GetAppBasic(ctx, name)
315+
if err != nil {
316+
if api.IsNotFoundError(err) || graphql.IsNotFoundError(err) {
317+
return false, nil
318+
}
319+
return false, err
320+
}
321+
322+
return true, nil
323+
}
324+
280325
// determineOrg returns the org specified on the command line, or the personal org if left unspecified
281326
func determineOrg(ctx context.Context) (*api.Organization, string, error) {
282327
var (

internal/haikunator/haikunator.go

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package haikunator
2+
3+
import (
4+
"crypto/rand"
5+
"math/big"
6+
rand2 "math/rand"
7+
"strconv"
8+
"strings"
9+
10+
"github.com/superfly/flyctl/helpers"
11+
)
12+
13+
var adjectives = strings.Fields(`
14+
autumn hidden bitter misty silent empty dry dark summer
15+
icy delicate quiet white cool spring winter patient
16+
twilight dawn crimson wispy weathered blue billowing
17+
broken cold damp falling frosty green long late lingering
18+
bold little morning muddy old red rough still small
19+
sparkling thrumming shy wandering withered wild black
20+
young holy solitary fragrant aged snowy proud floral
21+
restless divine polished ancient purple lively nameless
22+
`)
23+
var nouns = strings.Fields(`
24+
waterfall river breeze moon rain wind sea morning
25+
snow lake sunset pine shadow leaf dawn glitter forest
26+
hill cloud meadow sun glade bird brook butterfly
27+
bush dew dust field fire flower firefly feather grass
28+
haze mountain night pond darkness snowflake silence
29+
sound sky shape surf thunder violet water wildflower
30+
wave water resonance sun log dream cherry tree fog
31+
frost voice paper frog smoke star
32+
`)
33+
34+
type builder struct {
35+
tokRange int
36+
delimiter string
37+
}
38+
39+
type Builder interface {
40+
TokenRange(r int) Builder
41+
Delimiter(d string) Builder
42+
Build() string
43+
String() string
44+
}
45+
46+
func randN(max int) int {
47+
ret, err := rand.Int(rand.Reader, big.NewInt(int64(max)))
48+
if err != nil {
49+
// Fallback to "insecure" random
50+
// it doesn't really matter, this is not security critical
51+
return rand2.Intn(max) // skipcq: GSC-G404
52+
}
53+
return int(ret.Int64())
54+
}
55+
56+
func choose(list []string) string {
57+
return list[randN(len(list))]
58+
}
59+
60+
func Haikunator() Builder {
61+
return &builder{
62+
tokRange: 9999,
63+
delimiter: "-",
64+
}
65+
}
66+
67+
func (b *builder) TokenRange(r int) Builder {
68+
newB := helpers.Clone(b)
69+
newB.tokRange = r
70+
return newB
71+
}
72+
func (b *builder) Delimiter(d string) Builder {
73+
newB := helpers.Clone(b)
74+
newB.delimiter = d
75+
return newB
76+
}
77+
func (b *builder) Build() string {
78+
sections := []string{
79+
choose(adjectives),
80+
choose(nouns),
81+
}
82+
if b.tokRange > 0 {
83+
sections = append(sections, strconv.Itoa(randN(b.tokRange)))
84+
}
85+
return strings.Join(sections, b.delimiter)
86+
}
87+
func (b *builder) String() string {
88+
return b.Build()
89+
}

0 commit comments

Comments
 (0)