Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/cmd/cli/command/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@ func SetupCommands(version string) {
_ = configSetCmd.Flags().MarkHidden("name")
configSetCmd.Flags().BoolP("env", "e", false, "set the config from an environment variable")
configSetCmd.Flags().Bool("random", false, "set a secure randomly generated value for config")
configSetCmd.Flags().Bool("if-not-set", false, "set the config if it is not already set")
configSetCmd.Flags().String("env-file", "", "load config values from an .env file")
configSetCmd.MarkFlagFilename("env-file")

Expand Down
8 changes: 7 additions & 1 deletion src/cmd/cli/command/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ var configSetCmd = &cobra.Command{
ctx := cmd.Context()
fromEnv, _ := cmd.Flags().GetBool("env")
random, _ := cmd.Flags().GetBool("random")
ifNotSet, _ := cmd.Flags().GetBool("if-not-set")
envFile, _ := cmd.Flags().GetString("env-file")

// This command has several modes of operation:
Expand Down Expand Up @@ -167,8 +168,13 @@ var configSetCmd = &cobra.Command{

var errs []error
for name, value := range envMap {
if err := cli.ConfigSet(cmd.Context(), projectName, session.Provider, name, value); err != nil {
didSet, err := cli.ConfigSet(cmd.Context(), projectName, session.Provider, name, value, cli.ConfigSetOptions{
IfNotSet: ifNotSet,
})
if err != nil {
errs = append(errs, err)
} else if ifNotSet && !didSet {
term.Info("Config", name, "is already set; skipping due to --if-not-set flag")
} else {
term.Info("Updated value for", name)
}
Expand Down
3 changes: 2 additions & 1 deletion src/pkg/agent/tools/default_tool_cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ func (DefaultToolCLI) CanIUseProvider(ctx context.Context, fabric *client.GrpcCl
}

func (DefaultToolCLI) ConfigSet(ctx context.Context, projectName string, provider client.Provider, name, value string) error {
return cli.ConfigSet(ctx, projectName, provider, name, value)
_, err := cli.ConfigSet(ctx, projectName, provider, name, value, cli.ConfigSetOptions{})
return err
}

func (DefaultToolCLI) RunEstimate(ctx context.Context, project *compose.Project, fabric *client.GrpcClient, provider client.Provider, providerId client.ProviderID, region string, mode modes.Mode) (*defangv1.EstimateResponse, error) {
Expand Down
35 changes: 30 additions & 5 deletions src/pkg/cli/configSet.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"fmt"

"github.com/DefangLabs/defang/src/pkg"
"github.com/DefangLabs/defang/src/pkg/cli/client"
"github.com/DefangLabs/defang/src/pkg/dryrun"
"github.com/DefangLabs/defang/src/pkg/term"
defangv1 "github.com/DefangLabs/defang/src/protos/io/defang/v1"
Expand All @@ -19,16 +18,42 @@ func (e ErrInvalidConfigName) Error() string {
return fmt.Sprintf("invalid config name; must be alphanumeric or _, cannot start with a number: %q", e.Name)
}

func ConfigSet(ctx context.Context, projectName string, provider client.Provider, name string, value string) error {
type ConfigSetOptions struct {
IfNotSet bool
}

type ConfigManager interface {
ListConfig(ctx context.Context, req *defangv1.ListConfigsRequest) (*defangv1.Secrets, error)
PutConfig(ctx context.Context, req *defangv1.PutConfigRequest) error
}

func ConfigSet(ctx context.Context, projectName string, provider ConfigManager, name string, value string, options ConfigSetOptions) (bool, error) {
term.Debugf("Setting config %q in project %q", name, projectName)

if !pkg.IsValidSecretName(name) {
return ErrInvalidConfigName{Name: name}
return false, ErrInvalidConfigName{Name: name}
}

if dryrun.DoDryRun {
return dryrun.ErrDryRun
return false, dryrun.ErrDryRun
}

return provider.PutConfig(ctx, &defangv1.PutConfigRequest{Project: projectName, Name: name, Value: value})
if options.IfNotSet {
// Check if the config is already set
listResp, err := provider.ListConfig(ctx, &defangv1.ListConfigsRequest{Project: projectName})
if err != nil {
return false, fmt.Errorf("failed to get existing config %q: %w", name, err)
}
for _, existingName := range listResp.Names {
if existingName == name {
return false, nil
}
}
}

err := provider.PutConfig(ctx, &defangv1.PutConfigRequest{Project: projectName, Name: name, Value: value})
if err != nil {
return false, err
}
return true, nil
}
122 changes: 100 additions & 22 deletions src/pkg/cli/configSet_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,42 +2,120 @@ package cli

import (
"context"
"errors"
"testing"

"github.com/DefangLabs/defang/src/pkg/cli/client"
"github.com/DefangLabs/defang/src/pkg/dryrun"
defangv1 "github.com/DefangLabs/defang/src/protos/io/defang/v1"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)

type mockConfigManager struct {
mock.Mock
}

func (m mockConfigManager) ListConfig(ctx context.Context, req *defangv1.ListConfigsRequest) (*defangv1.Secrets, error) {
args := m.Called(ctx, req)
secret, ok := args.Get(0).(*defangv1.Secrets)
if !ok && args.Get(0) != nil {
return nil, args.Error(1)
}
return secret, args.Error(1)
}

func (m mockConfigManager) PutConfig(ctx context.Context, req *defangv1.PutConfigRequest) error {
args := m.Called(ctx, req)
return args.Error(0)
}

func TestConfigSet(t *testing.T) {
ctx := t.Context()
provider := MustHaveProjectNamePutConfigProvider{}

provider := &mockConfigManager{}

tests := []struct {
name string
configName string
existing []string
ifNotSet bool
expectedError error
expectedDidSet bool
}{
{
name: "config not set, should set",
configName: "test",
existing: []string{},
ifNotSet: true,
expectedDidSet: true,
},
{
name: "config already set, should skip",
configName: "test",
existing: []string{"test"},
ifNotSet: true,
expectedDidSet: false,
},
{
name: "config already set, should overwrite",
configName: "test",
existing: []string{"test_name"},
ifNotSet: false,
expectedDidSet: true,
},
{
name: "invalid config name, should error",
configName: "123invalid",
expectedError: ErrInvalidConfigName{Name: "123invalid"},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
project := "test"
value := "test_value"
provider.ExpectedCalls = nil // Reset expectations

provider.On("ListConfig", mock.Anything, &defangv1.ListConfigsRequest{Project: project}).
Maybe().
Return(&defangv1.Secrets{Names: tt.existing}, nil)

provider.On("PutConfig", mock.Anything, &defangv1.PutConfigRequest{
Project: project,
Name: tt.configName,
Value: value,
}).Maybe().Return(nil)

didSet, err := ConfigSet(ctx, project, provider, tt.configName, value, ConfigSetOptions{
IfNotSet: tt.ifNotSet,
})

if tt.expectedError != nil {
require.ErrorContains(t, err, tt.expectedError.Error())
}
assert.Equal(t, tt.expectedDidSet, didSet)

provider.AssertExpectations(t)
})
}

t.Run("expect no error", func(t *testing.T) {
err := ConfigSet(ctx, "test", provider, "test_name", "test_value")
if err != nil {
t.Fatalf("ConfigSet() error = %v", err)
}
provider.ExpectedCalls = nil // Reset expectations
provider.On("PutConfig", mock.Anything, &defangv1.PutConfigRequest{
Project: "test",
Name: "test_name",
Value: "test_value",
}).Return(nil)

_, err := ConfigSet(ctx, "test", provider, "test_name", "test_value", ConfigSetOptions{})
require.NoError(t, err)
provider.AssertExpectations(t)
})

t.Run("expect error on DryRun", func(t *testing.T) {
dryrun.DoDryRun = true
t.Cleanup(func() { dryrun.DoDryRun = false })
err := ConfigSet(ctx, "test", provider, "test_name", "test_value")
if err != dryrun.ErrDryRun {
t.Fatalf("Expected dryrun.ErrDryRun, got %v", err)
}
_, err := ConfigSet(ctx, "test", provider, "test_name", "test_value", ConfigSetOptions{})
require.ErrorIs(t, err, dryrun.ErrDryRun)
})
}

type MustHaveProjectNamePutConfigProvider struct {
client.Provider
}

func (m MustHaveProjectNamePutConfigProvider) PutConfig(ctx context.Context, req *defangv1.PutConfigRequest) error {
if req.Project == "" {
return errors.New("project name is missing")
}
return nil
}
Loading