Skip to content

Commit fab0ce7

Browse files
authored
Merge pull request #599 from DefangLabs/jordan/586
Add --project-name cli flag. Lazily load project name from flags, env, or compose file
2 parents 97c70c1 + 2b442b4 commit fab0ce7

File tree

15 files changed

+228
-94
lines changed

15 files changed

+228
-94
lines changed

src/cmd/cli/command/commands.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ func Execute(ctx context.Context) error {
8383
}
8484

8585
if err.Error() == "resource_exhausted: maximum number of projects reached" {
86-
printDefangHint("To deactivate a project, do:", "compose down")
86+
printDefangHint("To deactivate a project, do:", "compose down --project-name <name>")
8787
}
8888

8989
var cerr *cli.CancelError
@@ -134,6 +134,7 @@ func SetupCommands(version string) {
134134
RootCmd.PersistentFlags().BoolVar(&doDebug, "debug", pkg.GetenvBool("DEFANG_DEBUG"), "debug logging for troubleshooting the CLI")
135135
RootCmd.PersistentFlags().BoolVar(&cli.DoDryRun, "dry-run", false, "dry run (don't actually change anything)")
136136
RootCmd.PersistentFlags().BoolVarP(&nonInteractive, "non-interactive", "T", !hasTty, "disable interactive prompts / no TTY")
137+
RootCmd.PersistentFlags().StringP("project-name", "p", "", "project name")
137138
RootCmd.PersistentFlags().StringP("cwd", "C", "", "change directory before running the command")
138139
_ = RootCmd.MarkPersistentFlagDirname("cwd")
139140
RootCmd.PersistentFlags().StringArrayP("file", "f", []string{}, `compose file path`)
@@ -201,7 +202,6 @@ func SetupCommands(version string) {
201202
// composeCmd.Flags().Int("parallel", -1, "Control max parallelism, -1 for unlimited (default -1)"); TODO: Implement compose option
202203
// composeCmd.Flags().String("profile", "", "Specify a profile to enable"); TODO: Implement compose option
203204
// composeCmd.Flags().String("project-directory", "", "Specify an alternate working directory"); TODO: Implement compose option
204-
// composeCmd.Flags().StringP("project", "p", "", "Compose project name"); TODO: Implement compose option
205205
composeUpCmd.Flags().Bool("tail", false, "tail the service logs after updating") // obsolete, but keep for backwards compatibility
206206
_ = composeUpCmd.Flags().MarkHidden("tail")
207207
composeUpCmd.Flags().Bool("force", false, "force a build of the image even if nothing has changed")
@@ -587,7 +587,7 @@ var generateCmd = &cobra.Command{
587587
ConfigPaths: []string{filepath.Join(prompt.Folder, "compose.yaml")},
588588
}
589589
loader := compose.NewLoaderWithOptions(loaderOptions)
590-
project, _ := loader.LoadCompose(cmd.Context())
590+
project, _ := loader.LoadProject(cmd.Context())
591591

592592
var envInstructions []string
593593
for _, envVar := range collectUnsetEnvVars(project) {
@@ -1286,6 +1286,10 @@ func configureLoader(cmd *cobra.Command) compose.Loader {
12861286
panic(err)
12871287
}
12881288

1289+
o.ProjectName, err = f.GetString("project-name")
1290+
if err != nil {
1291+
panic(err)
1292+
}
12891293
return compose.NewLoaderWithOptions(o)
12901294
}
12911295

src/pkg/cli/client/byoc/aws/byoc.go

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,7 @@ func (b *ByocAws) environment() map[string]string {
316316
"DEFANG_ORG": b.TenantID,
317317
"DOMAIN": b.ProjectDomain,
318318
"PRIVATE_DOMAIN": b.PrivateDomain,
319-
"PROJECT": b.PulumiProject, // may be empty
319+
"PROJECT": b.ProjectName, // may be empty
320320
"PULUMI_BACKEND_URL": fmt.Sprintf(`s3://%s?region=%s&awssdk=v2`, b.bucketName(), region),
321321
"PULUMI_CONFIG_PASSPHRASE": pkg.Getenv("PULUMI_CONFIG_PASSPHRASE", "asdf"), // TODO: make customizable
322322
"STACK": b.PulumiStack,
@@ -356,8 +356,8 @@ func (b *ByocAws) Delete(ctx context.Context, req *defangv1.DeleteRequest) (*def
356356

357357
// stackDir returns a stack-qualified path, like the Pulumi TS function `stackDir`
358358
func (b *ByocAws) stackDir(name string) string {
359-
ensure(b.PulumiProject != "", "pulumiProject not set")
360-
return fmt.Sprintf("/%s/%s/%s/%s", byoc.DefangPrefix, b.PulumiProject, b.PulumiStack, name) // same as shared/common.ts
359+
ensure(b.ProjectName != "", "pulumiProject not set")
360+
return fmt.Sprintf("/%s/%s/%s/%s", byoc.DefangPrefix, b.ProjectName, b.PulumiStack, name) // same as shared/common.ts
361361
}
362362

363363
func (b *ByocAws) GetServices(ctx context.Context) (*defangv1.ListServicesResponse, error) {
@@ -376,8 +376,8 @@ func (b *ByocAws) GetServices(ctx context.Context) (*defangv1.ListServicesRespon
376376

377377
s3Client := s3.NewFromConfig(cfg)
378378
// Path to the state file, Defined at: https://github.com/DefangLabs/defang-mvp/blob/main/pulumi/cd/byoc/aws/index.ts#L89
379-
ensure(b.PulumiProject != "", "pulumiProject not set")
380-
path := fmt.Sprintf("projects/%s/%s/project.pb", b.PulumiProject, b.PulumiStack)
379+
ensure(b.ProjectName != "", "ProjectName not set")
380+
path := fmt.Sprintf("projects/%s/%s/project.pb", b.ProjectName, b.PulumiStack)
381381

382382
term.Debug("Getting services from bucket:", bucketName, path)
383383
getObjectOutput, err := s3Client.GetObject(ctx, &s3.GetObjectInput{
@@ -518,11 +518,11 @@ func (b *ByocAws) update(ctx context.Context, service *defangv1.Service) (*defan
518518
return nil, fmt.Errorf("missing config %q", missing) // retryable CodeFailedPrecondition
519519
}
520520

521-
ensure(b.PulumiProject != "", "pulumiProject not set")
521+
ensure(b.ProjectName != "", "ProjectName not set")
522522
si := &defangv1.ServiceInfo{
523523
Service: service,
524-
Project: b.PulumiProject, // was: tenant
525-
Etag: pkg.RandomID(), // TODO: could be hash for dedup/idempotency
524+
Project: b.ProjectName, // was: tenant
525+
Etag: pkg.RandomID(), // TODO: could be hash for dedup/idempotency
526526
}
527527

528528
hasHost := false
@@ -636,10 +636,10 @@ func (b *ByocAws) getPrivateFqdn(fqn qualifiedName) string {
636636
}
637637

638638
func (b *ByocAws) getProjectDomain(zone string) string {
639-
if b.PulumiProject == "" {
639+
if b.ProjectName == "" {
640640
return "" // no project name => no custom domain
641641
}
642-
projectLabel := byoc.DnsSafeLabel(b.PulumiProject)
642+
projectLabel := byoc.DnsSafeLabel(b.ProjectName)
643643
if projectLabel == byoc.DnsSafeLabel(b.TenantID) {
644644
return byoc.DnsSafe(zone) // the zone will already have the tenant ID
645645
}

src/pkg/cli/client/byoc/aws/byoc_test.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,10 @@ type FakeLoader struct {
6767
ProjectName string
6868
}
6969

70-
func (f FakeLoader) LoadCompose(ctx context.Context) (*compose.Project, error) {
70+
func (f FakeLoader) LoadProject(ctx context.Context) (*compose.Project, error) {
7171
return &compose.Project{Name: f.ProjectName}, nil
7272
}
73+
74+
func (f FakeLoader) LoadProjectName(ctx context.Context) (string, error) {
75+
return f.ProjectName, nil
76+
}

src/pkg/cli/client/byoc/baseclient.go

Lines changed: 41 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import (
77
"os"
88
"slices"
99
"strings"
10-
"sync"
1110

1211
"github.com/DefangLabs/defang/src/pkg"
1312
"github.com/DefangLabs/defang/src/pkg/cli/client"
@@ -49,23 +48,23 @@ type ByocBaseClient struct {
4948
PrivateLbIps []string // TODO: use API to get these
5049
PrivateNatIps []string // TODO: use API to get these
5150
ProjectDomain string
52-
PulumiProject string
51+
ProjectName string
5352
PulumiStack string
5453
Quota quota.Quotas
5554
SetupDone bool
5655
ShouldDelegateSubdomain bool
5756
TenantID string
5857

59-
loadProjOnce func() (*compose.Project, error)
58+
project *compose.Project
6059
bootstrapLister BootstrapLister
6160
}
6261

6362
func NewByocBaseClient(ctx context.Context, grpcClient client.GrpcClient, tenantID types.TenantID, bl BootstrapLister) *ByocBaseClient {
6463
b := &ByocBaseClient{
65-
GrpcClient: grpcClient,
66-
TenantID: string(tenantID),
67-
PulumiProject: "", // To be overwritten by LoadProject
68-
PulumiStack: "beta", // TODO: make customizable
64+
GrpcClient: grpcClient,
65+
TenantID: string(tenantID),
66+
ProjectName: "", // To be overwritten by LoadProject
67+
PulumiStack: "beta", // TODO: make customizable
6968
Quota: quota.Quotas{
7069
// These serve mostly to pevent fat-finger errors in the CLI or Compose files
7170
ServiceQuotas: quota.ServiceQuotas{
@@ -82,15 +81,6 @@ func NewByocBaseClient(ctx context.Context, grpcClient client.GrpcClient, tenant
8281
},
8382
bootstrapLister: bl,
8483
}
85-
b.loadProjOnce = sync.OnceValues(func() (*compose.Project, error) {
86-
proj, err := b.GrpcClient.Loader.LoadCompose(ctx)
87-
if err != nil {
88-
return nil, err
89-
}
90-
b.PrivateDomain = DnsSafeLabel(proj.Name) + ".internal"
91-
b.PulumiProject = proj.Name
92-
return proj, nil
93-
})
9484
return b
9585
}
9686

@@ -104,20 +94,42 @@ func (b *ByocBaseClient) GetVersions(context.Context) (*defangv1.Version, error)
10494
}
10595

10696
func (b *ByocBaseClient) LoadProject(ctx context.Context) (*compose.Project, error) {
107-
return b.loadProjOnce()
97+
if b.project != nil {
98+
return b.project, nil
99+
}
100+
project, err := b.Loader.LoadProject(ctx)
101+
if err != nil {
102+
return nil, err
103+
}
104+
105+
b.project = project
106+
b.setProjectName(project.Name)
107+
108+
return project, nil
108109
}
109110

110111
func (b *ByocBaseClient) LoadProjectName(ctx context.Context) (string, error) {
111-
112-
proj, err := b.loadProjOnce()
113-
if err == nil {
114-
b.PulumiProject = proj.Name
115-
return proj.Name, nil
112+
if b.ProjectName != "" {
113+
return b.ProjectName, nil
116114
}
117-
if !errors.Is(err, types.ErrComposeFileNotFound) {
115+
projectName, err := b.Loader.LoadProjectName(ctx) // Load the project to get the name
116+
if err != nil {
117+
if errors.Is(err, types.ErrComposeFileNotFound) {
118+
return b.loadProjectNameFromRemote(ctx)
119+
}
120+
118121
return "", err
119122
}
120123

124+
b.setProjectName(projectName)
125+
return projectName, nil
126+
}
127+
128+
func (b *ByocBaseClient) ServiceDNS(name string) string {
129+
return DnsSafeLabel(name) // TODO: consider merging this with getPrivateFqdn
130+
}
131+
132+
func (b *ByocBaseClient) loadProjectNameFromRemote(ctx context.Context) (string, error) {
121133
// Get the list of projects from remote
122134
projectNames, err := b.bootstrapLister.BootstrapList(ctx)
123135
if err != nil {
@@ -132,7 +144,7 @@ func (b *ByocBaseClient) LoadProjectName(ctx context.Context) (string, error) {
132144
}
133145
if len(projectNames) == 1 {
134146
term.Debug("Using default project: ", projectNames[0])
135-
b.PulumiProject = projectNames[0]
147+
b.setProjectName(projectNames[0])
136148
return projectNames[0], nil
137149
}
138150

@@ -141,14 +153,15 @@ func (b *ByocBaseClient) LoadProjectName(ctx context.Context) (string, error) {
141153
if !slices.Contains(projectNames, projectName) {
142154
return "", fmt.Errorf("project %q specified by COMPOSE_PROJECT_NAME not found", projectName)
143155
}
144-
term.Debug("Using project from COMPOSE_PROJECT_NAME environment variable:", projectNames[0])
145-
b.PulumiProject = projectName
156+
term.Debug("Using project from COMPOSE_PROJECT_NAME environment variable:", projectName)
157+
b.setProjectName(projectName)
146158
return projectName, nil
147159
}
148160

149161
return "", errors.New("multiple projects found; please go to the correct project directory where the compose file is or set COMPOSE_PROJECT_NAME")
150162
}
151163

152-
func (b *ByocBaseClient) ServiceDNS(name string) string {
153-
return DnsSafeLabel(name) // TODO: consider merging this with getPrivateFqdn
164+
func (b *ByocBaseClient) setProjectName(projectName string) {
165+
b.ProjectName = projectName
166+
b.PrivateDomain = DnsSafeLabel(b.ProjectName) + ".internal"
154167
}

src/pkg/cli/client/byoc/do/byoc.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ func (b *ByocDo) environment() map[string]string {
235235
"DEFANG_ORG": b.TenantID,
236236
"DOMAIN": b.ProjectDomain,
237237
"PRIVATE_DOMAIN": b.PrivateDomain,
238-
"PROJECT": b.PulumiProject,
238+
"PROJECT": b.ProjectName,
239239
"PULUMI_BACKEND_URL": fmt.Sprintf(`s3://%s.digitaloceanspaces.com/%s`, region, b.driver.BucketName), // TODO: add a way to override bucket
240240
"PULUMI_CONFIG_PASSPHRASE": pkg.Getenv("PULUMI_CONFIG_PASSPHRASE", "asdf"), // TODO: make customizable
241241
"STACK": b.PulumiStack,
@@ -251,7 +251,7 @@ func (b *ByocDo) update(ctx context.Context, service *defangv1.Service) (*defang
251251

252252
si := &defangv1.ServiceInfo{
253253
Service: service,
254-
Project: b.PulumiProject,
254+
Project: b.ProjectName,
255255
Etag: pkg.RandomID(),
256256
}
257257

src/pkg/cli/client/client.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ type ServerStream[Res any] interface {
1616
}
1717

1818
type ProjectLoader interface {
19-
LoadCompose(context.Context) (*compose.Project, error)
19+
LoadProjectName(context.Context) (string, error)
20+
LoadProject(context.Context) (*compose.Project, error)
2021
}
2122

2223
type FabricClient interface {

src/pkg/cli/client/playground.go

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,16 @@ import (
1515

1616
type PlaygroundClient struct {
1717
GrpcClient
18+
project *compose.Project
19+
projectName string
1820
}
1921

2022
func (g PlaygroundClient) LoadProject(ctx context.Context) (*compose.Project, error) {
21-
return g.Loader.LoadCompose(ctx)
23+
if g.project != nil {
24+
return g.project, nil
25+
}
26+
27+
return g.Loader.LoadProject(ctx)
2228
}
2329

2430
func (g PlaygroundClient) Deploy(ctx context.Context, req *defangv1.DeployRequest) (*defangv1.DeployResponse, error) {
@@ -126,9 +132,13 @@ func (g PlaygroundClient) ServiceDNS(name string) string {
126132
}
127133

128134
func (g PlaygroundClient) LoadProjectName(ctx context.Context) (string, error) {
129-
proj, err := g.Loader.LoadCompose(ctx)
135+
if g.projectName != "" {
136+
return g.projectName, nil
137+
}
138+
139+
name, err := g.Loader.LoadProjectName(ctx)
130140
if err == nil {
131-
return proj.Name, nil
141+
return name, nil
132142
}
133143
if !errors.Is(err, types.ErrComposeFileNotFound) {
134144
return "", err

0 commit comments

Comments
 (0)