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 cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ func getWorkerNamespace() components.Namespace {
commands.GetRemoveCommand(),
commands.GetListCommand(),
commands.GetAddSecretCommand(),
commands.GetAddPropertyCommand(),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we need a new command and not set them directly on the manifest ? like for filter

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar behaviour as secret, maybe useful for automation or if you dont want to trifle with the manifest syntax.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok

commands.GetListEventsCommand(),
commands.GetEditScheduleCommand(),
commands.GetShowExecutionHistoryCommand(),
Expand Down
106 changes: 106 additions & 0 deletions commands/add_property_cmd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// Package commands provides JFrog platform services worker management commands.
package commands

import (
"fmt"
"os"

"github.com/jfrog/jfrog-cli-platform-services/commands/common"

plugins_common "github.com/jfrog/jfrog-cli-core/v2/plugins/common"
"github.com/jfrog/jfrog-cli-core/v2/plugins/components"
"github.com/jfrog/jfrog-cli-core/v2/utils/ioutils"
"github.com/jfrog/jfrog-client-go/utils/log"

"github.com/jfrog/jfrog-cli-platform-services/model"
)

type addPropertyCommand struct {
ctx *components.Context
}

func GetAddPropertyCommand() components.Command {
return components.Command{
Name: "add-property",
Description: "Add a clear-text property to a worker",
AIDescription: `Add or update a clear-text property in the local manifest.json. Pass the value as a second argument, or omit it to prompt or use JFROG_WORKER_CLI_DEV_ADD_PROPERTY_VALUE. This command only writes locally; run 'jf worker deploy' to send the property to the server.

Properties are not secrets: values remain unencrypted and readable on disk. Omitting manifest.properties preserves remote properties, while an explicit empty object clears them on the next deploy.`,
Aliases: []string{"ap"},
Flags: []components.Flag{
components.NewBoolFlag(model.FlagEdit, "Whether to update an existing property.", components.WithBoolDefaultValue(false)),
},
Arguments: []components.Argument{
{
Name: "property-name",
Description: "The property name.",
},
{
Name: "property-value",
Description: "The property value. If omitted, prompted or taken from JFROG_WORKER_CLI_DEV_ADD_PROPERTY_VALUE.",
Optional: true,
},
},
Action: func(c *components.Context) error {
return (&addPropertyCommand{ctx: c}).run()
},
}
}

func (c *addPropertyCommand) run() error {
manifest, err := common.ReadManifest()
if err != nil {
return err
}
if err = common.ValidateManifest(manifest, nil); err != nil {
return err
}

propertyName, err := c.getPropertyName()
if err != nil {
return err
}
if err = c.checkUpdate(manifest, propertyName); err != nil {
return err
}

propertyValue := c.readPropertyValue()
if manifest.Properties == nil {
manifest.Properties = map[string]string{}
}
manifest.Properties[propertyName] = propertyValue

if err = common.SaveManifest(manifest); err != nil {
return err
}

log.Info(fmt.Sprintf("Property '%s' saved", propertyName))
return nil
}

func (c *addPropertyCommand) getPropertyName() (string, error) {
if len(c.ctx.Arguments) < 1 || len(c.ctx.Arguments) > 2 {
return "", plugins_common.WrongNumberOfArgumentsHandler(c.ctx)
}
return c.ctx.Arguments[0], nil
}

func (c *addPropertyCommand) checkUpdate(manifest *model.Manifest, propertyName string) error {
if _, exists := manifest.Properties[propertyName]; exists && !c.ctx.GetBoolFlagValue(model.FlagEdit) {
return fmt.Errorf("%s already exists, use --%s to overwrite", propertyName, model.FlagEdit)
}
return nil
}

func (c *addPropertyCommand) readPropertyValue() string {
if len(c.ctx.Arguments) > 1 {
return c.ctx.Arguments[1]
}
if propertyValue, exists := os.LookupEnv(model.EnvKeyAddPropertyValue); exists {
return propertyValue
}

var propertyValue string
ioutils.ScanFromConsole("Value", &propertyValue, "")
return propertyValue
}
122 changes: 122 additions & 0 deletions commands/add_property_cmd_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
//go:build test
// +build test

package commands

import (
"fmt"
"os"
"testing"

"github.com/jfrog/jfrog-cli-platform-services/commands/common"
"github.com/jfrog/jfrog-cli-platform-services/model"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestAddPropertyCmd(t *testing.T) {
tests := []struct {
name string
commandArgs []string
propertyName string
propertyValue string
wantErr string
want map[string]string
patchManifest func(mf *model.Manifest)
}{
{
name: "add",
propertyName: "prop-1",
propertyValue: "value-1",
patchManifest: func(mf *model.Manifest) {
mf.Properties = map[string]string{"prop-2": "value-2"}
},
want: map[string]string{"prop-1": "value-1", "prop-2": "value-2"},
},
{
name: "add from argument",
commandArgs: []string{"prop-1", "value-1"},
patchManifest: func(mf *model.Manifest) {
mf.Properties = map[string]string{"prop-2": "value-2"}
},
want: map[string]string{"prop-1": "value-1", "prop-2": "value-2"},
},
{
name: "argument overrides env",
commandArgs: []string{"prop-1", "from-arg"},
propertyValue: "from-env",
want: map[string]string{"prop-1": "from-arg"},
},
{
name: "reject extra arguments",
commandArgs: []string{"prop-1", "value-1", "extra"},
wantErr: "Wrong number of arguments (3).",
},
{
name: "add with nil properties",
propertyName: "prop-1",
propertyValue: "value-1",
patchManifest: func(mf *model.Manifest) {
mf.Properties = nil
},
want: map[string]string{"prop-1": "value-1"},
},
{
name: "edit property",
propertyName: "prop-1",
propertyValue: "new-value",
commandArgs: []string{fmt.Sprintf("--%s", model.FlagEdit)},
patchManifest: func(mf *model.Manifest) {
mf.Properties = map[string]string{"prop-1": "old-value"}
},
want: map[string]string{"prop-1": "new-value"},
},
{
name: "reject duplicate without edit",
propertyName: "prop-1",
propertyValue: "new-value",
patchManifest: func(mf *model.Manifest) {
mf.Properties = map[string]string{"prop-1": "old-value"}
},
wantErr: "prop-1 already exists, use --edit to overwrite",
},
{
name: "reject missing name",
wantErr: "Wrong number of arguments (0).",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
common.NewMockWorkerServer(t, common.NewServerStub(t).WithDefaultActionsMetadataEndpoint())

workerDir, workerName := common.PrepareWorkerDirForTest(t)
runCmd := common.CreateCliRunner(t, GetInitCommand(), GetAddPropertyCommand())
require.NoError(t, runCmd("worker", "init", "GENERIC_EVENT", workerName))

if tt.patchManifest != nil {
common.PatchManifest(t, tt.patchManifest)
}
if tt.propertyValue != "" {
require.NoError(t, os.Setenv(model.EnvKeyAddPropertyValue, tt.propertyValue))
t.Cleanup(func() { _ = os.Unsetenv(model.EnvKeyAddPropertyValue) })
}

cmd := append([]string{"worker", "add-property"}, tt.commandArgs...)
if tt.propertyName != "" {
cmd = append(cmd, tt.propertyName)
}

err := runCmd(cmd...)
if tt.wantErr != "" {
assert.EqualError(t, err, tt.wantErr)
return
}

require.NoError(t, err)
manifest, err := common.ReadManifest(workerDir)
require.NoError(t, err)
assert.Equal(t, tt.want, manifest.Properties)
})
}
}
24 changes: 24 additions & 0 deletions commands/common/cmd_commons.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,27 @@ func PrepareSecretsUpdate(mf *model.Manifest, existingWorker *model.WorkerDetail

return secrets
}

func PreparePropertiesUpdate(mf *model.Manifest, existingWorker *model.WorkerDetails) []*model.Property {
removedProperties := map[string]any{}
if existingWorker != nil {
for _, existingProperty := range existingWorker.Properties {
removedProperties[existingProperty.Key] = struct{}{}
}
}

var properties []*model.Property
for propertyName, propertyValue := range mf.Properties {
if _, propertyExists := removedProperties[propertyName]; propertyExists {
properties = append(properties, &model.Property{Key: propertyName, MarkedForRemoval: true})
}
delete(removedProperties, propertyName)
properties = append(properties, &model.Property{Key: propertyName, Value: propertyValue})
}

for removedProperty := range removedProperties {
properties = append(properties, &model.Property{Key: removedProperty, MarkedForRemoval: true})
}

return properties
}
44 changes: 44 additions & 0 deletions commands/common/cmd_commons_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,50 @@ func Test_cleanImports(t *testing.T) {
}
}

func TestPreparePropertiesUpdate(t *testing.T) {
tests := []struct {
name string
properties map[string]string
existing []*model.Property
want []*model.Property
}{
{
name: "adds local properties",
properties: map[string]string{"new": "value"},
want: []*model.Property{{Key: "new", Value: "value"}},
},
{
name: "replaces existing and removes remote-only properties",
properties: map[string]string{"changed": "new-value"},
existing: []*model.Property{
{Key: "changed", Value: "old-value"},
{Key: "remote-only", Value: "remote-value"},
},
want: []*model.Property{
{Key: "changed", MarkedForRemoval: true},
{Key: "changed", Value: "new-value"},
{Key: "remote-only", MarkedForRemoval: true},
},
},
{
name: "empty map removes every remote property",
properties: map[string]string{},
existing: []*model.Property{{Key: "remote", Value: "value"}},
want: []*model.Property{{Key: "remote", MarkedForRemoval: true}},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := PreparePropertiesUpdate(
&model.Manifest{Properties: tt.properties},
&model.WorkerDetails{Properties: tt.existing},
)
assert.ElementsMatch(t, tt.want, got)
})
}
}

func Test_extractProjectAndKeyFromCommandContext(t *testing.T) {
tests := []struct {
name string
Expand Down
2 changes: 1 addition & 1 deletion commands/common/cmd_worker_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ func FetchWorkerDetails(c model.IntFlagProvider, serverURL string, accessToken s
if len(content) == 0 {
return nil
}
log.Info(fmt.Sprintf("Worker %s details returned from the server", details.Key))
log.Info(fmt.Sprintf("Worker %s details returned from the server", workerKey))
return json.Unmarshal(content, details)
},
})
Expand Down
35 changes: 35 additions & 0 deletions commands/common/manifest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,41 @@ func TestReadManifest(t *testing.T) {
}
}

func TestReadManifestPreservesOmittedAndEmptyProperties(t *testing.T) {
tests := []struct {
name string
properties string
assert func(*testing.T, map[string]string)
}{
{
name: "omitted",
assert: func(t *testing.T, properties map[string]string) {
assert.Nil(t, properties)
},
},
{
name: "empty",
properties: `,"properties":{}`,
assert: func(t *testing.T, properties map[string]string) {
assert.NotNil(t, properties)
assert.Empty(t, properties)
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
dir := t.TempDir()
manifest := `{"name":"worker","sourceCodePath":"worker.ts","action":"GENERIC_EVENT"` + tt.properties + `}`
require.NoError(t, os.WriteFile(filepath.Join(dir, "manifest.json"), []byte(manifest), 0o600))

got, err := ReadManifest(dir)
require.NoError(t, err)
tt.assert(t, got.Properties)
})
}
}

func TestManifest_ReadSourceCode(t *testing.T) {
tests := []struct {
name string
Expand Down
2 changes: 2 additions & 0 deletions commands/common/test_worker_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ type workerDeployPayload struct {
Action model.Action `json:"action"`
FilterCriteria *model.FilterCriteria `json:"filterCriteria,omitempty"`
Secrets []*model.Secret `json:"secrets"`
Properties []*model.Property `json:"properties,omitempty"`
ProjectKey string `json:"projectKey"`
Version *model.Version `json:"version,omitempty"`
}
Expand Down Expand Up @@ -641,6 +642,7 @@ func mapWorkerSentToWorkerDetails(workerSent workerDeployPayload) *model.WorkerD
Action: workerSent.Action.Name, // Map Action.Name
FilterCriteria: workerSent.FilterCriteria,
Secrets: workerSent.Secrets,
Properties: workerSent.Properties,
ProjectKey: workerSent.ProjectKey,
}
}
Loading
Loading