Skip to content

Commit a69ddb2

Browse files
authored
Add support for PULUMI_ACCESS_TOKEN (#1050)
* Add support for PULUMI_ACCESS_TOKEN * Return err if PULUMI_ACCESS_TOKEN is not set * Add Domainname field to ServiceInfo in BYOC update method * Change pulumi-backend flag to persistent in CD and compose commands * Improve error message for unset PULUMI_ACCESS_TOKEN in Pulumi Cloud
1 parent 9837bc5 commit a69ddb2

File tree

9 files changed

+231
-100
lines changed

9 files changed

+231
-100
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,9 +116,12 @@ The Defang CLI recognizes the following environment variables:
116116
- `DEFANG_NO_CACHE` - If set to `true`, disables pull-through caching of container images; defaults to `false`
117117
- `DEFANG_PREFIX` - The prefix to use for all BYOC resources; defaults to `Defang`
118118
- `DEFANG_PROVIDER` - The name of the cloud provider to use, `auto` (default), `aws`, `digitalocean`, `gcp`, or `defang`
119+
- `DEFANG_PULUMI_BACKEND` - The Pulumi backend URL or `"pulumi-cloud"`; defaults to a self-hosted backend
119120
- `DEFANG_PULUMI_DIR` - Run Pulumi from this folder, instead of spawning a cloud task; requires `--debug` (BYOC only)
120121
- `DEFANG_PULUMI_VERSION` - Override the version of the Pulumi image to use (`aws` provider only)
121122
- `NO_COLOR` - If set to any value, disables color output; by default, color output is enabled depending on the terminal
123+
- `PULUMI_ACCESS_TOKEN` - The Pulumi access token to use for authentication to Pulumi Cloud; see `DEFANG_PULUMI_BACKEND`
124+
- `PULUMI_CONFIG_PASSPHRASE` - Passphrase used to generate a unique key for your stack, and configuration and encrypted state values
122125
- `TZ` - The timezone to use for log timestamps: an IANA TZ name like `UTC` or `Europe/Amsterdam`; defaults to `Local`
123126
- `XDG_STATE_HOME` - The directory to use for storing state; defaults to `~/.local/state`
124127

src/cmd/cli/command/commands.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"github.com/DefangLabs/defang/src/pkg"
1919
"github.com/DefangLabs/defang/src/pkg/cli"
2020
cliClient "github.com/DefangLabs/defang/src/pkg/cli/client"
21+
"github.com/DefangLabs/defang/src/pkg/cli/client/byoc"
2122
"github.com/DefangLabs/defang/src/pkg/cli/compose"
2223
"github.com/DefangLabs/defang/src/pkg/clouds/aws"
2324
"github.com/DefangLabs/defang/src/pkg/logs"
@@ -173,6 +174,7 @@ func SetupCommands(ctx context.Context, version string) {
173174

174175
// CD command
175176
RootCmd.AddCommand(cdCmd)
177+
cdCmd.PersistentFlags().StringVar(&byoc.DefangPulumiBackend, "pulumi-backend", "", `specify an alternate Pulumi backend URL or "pulumi-cloud"`)
176178
cdCmd.AddCommand(cdDestroyCmd)
177179
cdCmd.AddCommand(cdDownCmd)
178180
cdCmd.AddCommand(cdRefreshCmd)

src/cmd/cli/command/compose.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"github.com/DefangLabs/defang/src/pkg"
1212
"github.com/DefangLabs/defang/src/pkg/cli"
1313
cliClient "github.com/DefangLabs/defang/src/pkg/cli/client"
14+
"github.com/DefangLabs/defang/src/pkg/cli/client/byoc"
1415
"github.com/DefangLabs/defang/src/pkg/cli/compose"
1516
"github.com/DefangLabs/defang/src/pkg/logs"
1617
"github.com/DefangLabs/defang/src/pkg/term"
@@ -521,6 +522,7 @@ services:
521522
// composeCmd.Flags().Int("parallel", -1, "Control max parallelism, -1 for unlimited (default -1)"); TODO: Implement compose option
522523
// composeCmd.Flags().String("profile", "", "Specify a profile to enable"); TODO: Implement compose option
523524
// composeCmd.Flags().String("project-directory", "", "Specify an alternate working directory"); TODO: Implement compose option
525+
composeCmd.PersistentFlags().StringVar(&byoc.DefangPulumiBackend, "pulumi-backend", "", `specify an alternate Pulumi backend URL or "pulumi-cloud"`)
524526
composeCmd.AddCommand(makeComposeUpCmd())
525527
composeCmd.AddCommand(makeComposeConfigCmd())
526528
composeCmd.AddCommand(makeComposeDownCmd())

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

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -399,18 +399,24 @@ func (b *ByocAws) bucketName() string {
399399
return pkg.Getenv("DEFANG_CD_BUCKET", b.driver.BucketName)
400400
}
401401

402-
func (b *ByocAws) environment(projectName string) map[string]string {
402+
func (b *ByocAws) environment(projectName string) (map[string]string, error) {
403403
region := b.driver.Region // TODO: this should be the destination region, not the CD region; make customizable
404+
defangStateUrl := fmt.Sprintf(`s3://%s?region=%s&awssdk=v2`, b.bucketName(), region)
405+
pulumiBackendKey, pulumiBackendValue, err := byoc.GetPulumiBackend(defangStateUrl)
406+
if err != nil {
407+
return nil, err
408+
}
404409
env := map[string]string{
405410
// "AWS_REGION": region.String(), should be set by ECS (because of CD task role)
406411
"DEFANG_DEBUG": os.Getenv("DEFANG_DEBUG"), // TODO: use the global DoDebug flag
407412
"DEFANG_ORG": b.TenantName,
408413
"DEFANG_PREFIX": byoc.DefangPrefix,
414+
"DEFANG_STATE_URL": defangStateUrl,
409415
"NPM_CONFIG_UPDATE_NOTIFIER": "false",
410416
"PRIVATE_DOMAIN": byoc.GetPrivateDomain(projectName),
411-
"PROJECT": projectName, // may be empty
412-
"PULUMI_BACKEND_URL": fmt.Sprintf(`s3://%s?region=%s&awssdk=v2`, b.bucketName(), region),
413-
"PULUMI_CONFIG_PASSPHRASE": pkg.Getenv("PULUMI_CONFIG_PASSPHRASE", "asdf"), // TODO: make customizable
417+
"PROJECT": projectName, // may be empty
418+
pulumiBackendKey: pulumiBackendValue, // TODO: make secret
419+
"PULUMI_CONFIG_PASSPHRASE": byoc.PulumiConfigPassphrase, // TODO: make secret
414420
"PULUMI_SKIP_UPDATE_CHECK": "true",
415421
"STACK": b.PulumiStack,
416422
}
@@ -419,7 +425,7 @@ func (b *ByocAws) environment(projectName string) map[string]string {
419425
env["NO_COLOR"] = "1"
420426
}
421427

422-
return env
428+
return env, nil
423429
}
424430

425431
type cdCmd struct {
@@ -432,7 +438,10 @@ type cdCmd struct {
432438

433439
func (b *ByocAws) runCdCommand(ctx context.Context, cmd cdCmd) (ecs.TaskArn, error) {
434440
// Setup the deployment environment
435-
env := b.environment(cmd.project)
441+
env, err := b.environment(cmd.project)
442+
if err != nil {
443+
return nil, err
444+
}
436445
if cmd.delegationSetId != "" {
437446
env["DELEGATION_SET_ID"] = cmd.delegationSetId
438447
}

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

Lines changed: 5 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@ import (
44
"context"
55
"errors"
66
"fmt"
7-
"os"
8-
"os/exec"
97
"strings"
108

119
"github.com/DefangLabs/defang/src/pkg"
@@ -17,25 +15,6 @@ import (
1715
composeTypes "github.com/compose-spec/compose-go/v2/types"
1816
)
1917

20-
const (
21-
CdDefaultImageTag = "public-beta" // for when a project has no cd version, this would be a old deployment
22-
CdLatestImageTag = "public-beta" // Update this to the latest CD service major version number whenever cd major is changed
23-
CdTaskPrefix = "defang-cd" // WARNING: renaming this practically deletes the Pulumi state
24-
)
25-
26-
var (
27-
DefangPrefix = pkg.Getenv("DEFANG_PREFIX", "Defang") // prefix for all resources created by Defang
28-
)
29-
30-
// This function was copied from Fabric controller and slightly modified to work with BYOC
31-
func DnsSafeLabel(fqn string) string {
32-
return strings.ReplaceAll(DnsSafe(fqn), ".", "-")
33-
}
34-
35-
func DnsSafe(fqdn string) string {
36-
return strings.ToLower(fqdn)
37-
}
38-
3918
type ErrMultipleProjects struct {
4019
ProjectNames []string
4120
}
@@ -82,37 +61,6 @@ func MakeEnv(key string, value any) string {
8261
return fmt.Sprintf("%s=%q", key, value)
8362
}
8463

85-
func runLocalCommand(ctx context.Context, dir string, env []string, cmd ...string) error {
86-
command := exec.CommandContext(ctx, cmd[0], cmd[1:]...)
87-
command.Dir = dir
88-
command.Env = env
89-
command.Stdout = os.Stdout
90-
command.Stderr = os.Stderr
91-
return command.Run()
92-
}
93-
94-
func DebugPulumi(ctx context.Context, env []string, cmd ...string) error {
95-
// Locally we use the "dev" script from package.json to run Pulumi commands, which uses ts-node
96-
localCmd := append([]string{"npm", "run", "dev"}, cmd...)
97-
term.Debug(strings.Join(append(env, localCmd...), " "))
98-
99-
dir := os.Getenv("DEFANG_PULUMI_DIR")
100-
if dir == "" {
101-
return nil // show the shell command, but use regular Pulumi command in cloud task
102-
}
103-
104-
// Run the Pulumi command locally
105-
env = append([]string{
106-
"PATH=" + os.Getenv("PATH"),
107-
"USER=" + pkg.GetCurrentUser(), // needed for Pulumi
108-
}, env...)
109-
if err := runLocalCommand(ctx, dir, env, localCmd...); err != nil {
110-
return err
111-
}
112-
// We always return an error to stop the CLI from "tailing" the cloud logs
113-
return errors.New("local pulumi command succeeded; stopping")
114-
}
115-
11664
func (b *ByocBaseClient) GetProjectLastCDImage(ctx context.Context, projectName string) (string, error) {
11765
projUpdate, err := b.projectBackend.GetProjectUpdate(ctx, projectName)
11866
if err != nil {
@@ -126,11 +74,6 @@ func (b *ByocBaseClient) GetProjectLastCDImage(ctx context.Context, projectName
12674
return projUpdate.CdVersion, nil
12775
}
12876

129-
func ExtractImageTag(fullQualifiedImageURI string) string {
130-
index := strings.LastIndex(fullQualifiedImageURI, ":")
131-
return fullQualifiedImageURI[index+1:]
132-
}
133-
13477
func (b *ByocBaseClient) Debug(context.Context, *defangv1.DebugRequest) (*defangv1.DebugResponse, error) {
13578
return nil, client.ErrNotImplemented("AI debugging is not yet supported for BYOC")
13679
}
@@ -139,11 +82,6 @@ func (b *ByocBaseClient) SetCDImage(image string) {
13982
b.CDImage = image
14083
}
14184

142-
func (b *ByocBaseClient) GetVersions(context.Context) (*defangv1.Version, error) {
143-
// we want only the latest version of the CD service this CLI was compiled to expect
144-
return &defangv1.Version{Fabric: CdLatestImageTag}, nil
145-
}
146-
14785
func (b *ByocBaseClient) ServiceDNS(name string) string {
14886
return DnsSafeLabel(name) // TODO: consider merging this with getPrivateFqdn
14987
}
@@ -194,7 +132,7 @@ func (b *ByocBaseClient) GetServiceInfos(ctx context.Context, projectName, deleg
194132
serviceInfo.Etag = etag // same etag for all services
195133
serviceInfoMap[service.Name] = &Node{
196134
Name: service.Name,
197-
Deps: getDependencies(service),
135+
Deps: service.GetDependencies(),
198136
ServiceInfo: serviceInfo,
199137
}
200138
}
@@ -234,14 +172,6 @@ func topologicalSort(nodes map[string]*Node) []*defangv1.ServiceInfo {
234172
return serviceInfos
235173
}
236174

237-
func getDependencies(service composeTypes.ServiceConfig) []string {
238-
deps := []string{}
239-
for depServiceName := range service.DependsOn {
240-
deps = append(deps, depServiceName)
241-
}
242-
return deps
243-
}
244-
245175
// This function was based on update function from Fabric controller and slightly modified to work with BYOC
246176
func (b *ByocBaseClient) update(ctx context.Context, projectName, delegateDomain string, service composeTypes.ServiceConfig) (*defangv1.ServiceInfo, error) {
247177
if err := compose.ValidateService(&service); err != nil {
@@ -250,9 +180,10 @@ func (b *ByocBaseClient) update(ctx context.Context, projectName, delegateDomain
250180

251181
pkg.Ensure(projectName != "", "ProjectName not set")
252182
si := &defangv1.ServiceInfo{
253-
Etag: pkg.RandomID(), // TODO: could be hash for dedup/idempotency
254-
Project: projectName, // was: tenant
255-
Service: &defangv1.Service{Name: service.Name},
183+
Etag: pkg.RandomID(), // TODO: could be hash for dedup/idempotency
184+
Project: projectName, // was: tenant
185+
Service: &defangv1.Service{Name: service.Name},
186+
Domainname: service.DomainName,
256187
}
257188

258189
hasHost := false
@@ -340,7 +271,3 @@ func (b ByocBaseClient) GetPrivateFqdn(projectName string, fqn string) string {
340271
safeFqn := DnsSafeLabel(fqn)
341272
return fmt.Sprintf("%s.%s", safeFqn, GetPrivateDomain(projectName)) // TODO: consider merging this with ServiceDNS
342273
}
343-
344-
func GetPrivateDomain(projectName string) string {
345-
return DnsSafeLabel(projectName) + ".internal"
346-
}

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

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package byoc
2+
3+
import (
4+
"context"
5+
"errors"
6+
"os"
7+
"os/exec"
8+
"strings"
9+
10+
"github.com/DefangLabs/defang/src/pkg"
11+
"github.com/DefangLabs/defang/src/pkg/term"
12+
)
13+
14+
const (
15+
CdTaskPrefix = "defang-cd" // WARNING: renaming this practically deletes the Pulumi state
16+
)
17+
18+
var (
19+
DefangPrefix = pkg.Getenv("DEFANG_PREFIX", "Defang") // prefix for all resources created by Defang
20+
DefangPulumiBackend = os.Getenv("DEFANG_PULUMI_BACKEND")
21+
PulumiConfigPassphrase = pkg.Getenv("PULUMI_CONFIG_PASSPHRASE", "asdf")
22+
)
23+
24+
func getPulumiAccessToken() (string, error) {
25+
pat := os.Getenv("PULUMI_ACCESS_TOKEN")
26+
if pat == "" {
27+
// TODO: could consider parsing ~/.pulumi/credentials.json
28+
return "", errors.New("PULUMI_ACCESS_TOKEN must be set for Pulumi Cloud")
29+
}
30+
return pat, nil
31+
}
32+
33+
func GetPulumiBackend(stateUrl string) (string, string, error) {
34+
switch strings.ToLower(DefangPulumiBackend) {
35+
case "pulumi-cloud":
36+
pat, err := getPulumiAccessToken()
37+
return "PULUMI_ACCESS_TOKEN", pat, err
38+
case "":
39+
return "PULUMI_BACKEND_URL", stateUrl, nil
40+
default:
41+
return "PULUMI_BACKEND_URL", DefangPulumiBackend, nil
42+
}
43+
}
44+
45+
// This function was copied from Fabric controller and slightly modified to work with BYOC
46+
func DnsSafeLabel(fqn string) string {
47+
return strings.ReplaceAll(DnsSafe(fqn), ".", "-")
48+
}
49+
50+
func DnsSafe(fqdn string) string {
51+
return strings.ToLower(fqdn)
52+
}
53+
54+
func runLocalCommand(ctx context.Context, dir string, env []string, cmd ...string) error {
55+
command := exec.CommandContext(ctx, cmd[0], cmd[1:]...)
56+
command.Dir = dir
57+
command.Env = env
58+
command.Stdout = os.Stdout
59+
command.Stderr = os.Stderr
60+
return command.Run()
61+
}
62+
63+
func DebugPulumi(ctx context.Context, env []string, cmd ...string) error {
64+
// Locally we use the "dev" script from package.json to run Pulumi commands, which uses ts-node
65+
localCmd := append([]string{"npm", "run", "dev"}, cmd...)
66+
term.Debug(strings.Join(append(env, localCmd...), " "))
67+
68+
dir := os.Getenv("DEFANG_PULUMI_DIR")
69+
if dir == "" {
70+
return nil // show the shell command, but use regular Pulumi command in cloud task
71+
}
72+
73+
// Run the Pulumi command locally
74+
env = append([]string{
75+
"PATH=" + os.Getenv("PATH"),
76+
"USER=" + pkg.GetCurrentUser(), // needed for Pulumi
77+
}, env...)
78+
if err := runLocalCommand(ctx, dir, env, localCmd...); err != nil {
79+
return err
80+
}
81+
// We always return an error to stop the CLI from "tailing" the cloud logs
82+
return errors.New("local pulumi command succeeded; stopping")
83+
}
84+
85+
func GetPrivateDomain(projectName string) string {
86+
return DnsSafeLabel(projectName) + ".internal"
87+
}

0 commit comments

Comments
 (0)