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
25 changes: 15 additions & 10 deletions commands/common/cmd_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,16 +43,17 @@ func apiError(status int, message string, args ...any) *APIError {
}

type APICallParams struct {
Method string
ServerURL string
ServerToken string
Body []byte
Query map[string]string
Path []string
ProjectKey string
APIVersion apiVersion
OkStatuses []int
OnContent APIContentHandler
Method string
ServerURL string
ServerToken string
Body []byte
Query map[string]string
Path []string
ProjectKey string
APIVersion apiVersion
OkStatuses []int
OnContent APIContentHandler
CaptureStatus *int
}

func CallWorkerAPI(c model.IntFlagProvider, params APICallParams) error {
Expand Down Expand Up @@ -113,6 +114,10 @@ func CallWorkerAPI(c model.IntFlagProvider, params APICallParams) error {
return apiError(res.StatusCode, "command %s %s returned an unexpected status code %d", params.Method, apiEndpoint, res.StatusCode)
}

if params.CaptureStatus != nil {
*params.CaptureStatus = res.StatusCode
}

return processAPIResponse(res, params.OnContent)
}

Expand Down
26 changes: 26 additions & 0 deletions commands/common/cmd_io.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,32 @@ func PrintJSON(data []byte) error {
return err
}

// PrintJSONValue marshals v to indented JSON and writes it to the CLI output.
// Use instead of json.Marshal + PrintJSON when the struct is already available.
func PrintJSONValue(v any) error {
data, err := json.MarshalIndent(v, "", " ")
if err != nil {
return err
}
_, err = cliOut.Write(data)
return err
}

// PrintJSONOrStatus prints contentBytes as JSON when it is valid JSON,
// otherwise prints {"status_code": statusCode, "content": "<contentBytes>"}.
func PrintJSONOrStatus(statusCode int, contentBytes []byte) error {
if len(contentBytes) > 0 && json.Valid(contentBytes) {
return PrintJSON(contentBytes)
}
return PrintJSONValue(struct {
StatusCode int `json:"status_code"`
Content string `json:"content"`
}{
StatusCode: statusCode,
Content: string(contentBytes),
})
}

func printJSONOrLogError(data []byte) error {
if _, writeErr := cliOut.Write(PrettifyJSON(data)); writeErr != nil {
log.Warn(fmt.Sprintf("Write error: %+v (data:%s)", writeErr, string(data)))
Expand Down
16 changes: 16 additions & 0 deletions commands/common/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package common

import (
"fmt"
"strings"

"github.com/jfrog/jfrog-cli-core/v2/common/format"
)

func ErrUnsupportedFormat(format format.OutputFormat, supportedFormats ...format.OutputFormat) error {
supportedFormatsStr := make([]string, len(supportedFormats))
for i, f := range supportedFormats {
supportedFormatsStr[i] = string(f)
}
return fmt.Errorf("unsupported format '%s'. Accepted values: %s", format, strings.Join(supportedFormatsStr, ", "))
}
19 changes: 10 additions & 9 deletions commands/common/test_worker_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,16 +85,17 @@ type queryParamStub struct {
paths []string
}

type ExecutionHistoryResultEntryStub struct {
Result string `json:"result"`
Logs string `json:"logs"`
}

type ExecutionHistoryEntryStub struct {
Start time.Time `json:"start"`
End time.Time `json:"end"`
TestRun bool `json:"testRun"`
Result ExecutionHistoryResultEntryStub `json:"entries"`
WorkerKey string `json:"workerKey"`
WorkerType string `json:"workerType"`
WorkerProjectKey string `json:"workerProjectKey"`
ExecutionStatus string `json:"executionStatus"`
StartTimeMillis int64 `json:"startTimeMillis"`
EndTimeMillis int64 `json:"endTimeMillis"`
TriggeredBy string `json:"triggeredBy"`
TestRun bool `json:"testRun"`
ExecutedVersion string `json:"executedVersion"`
TraceID string `json:"traceId"`
}

type ExecutionHistoryStub []*ExecutionHistoryEntryStub
Expand Down
138 changes: 91 additions & 47 deletions commands/deploy_cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import (
"encoding/json"
"fmt"
"net/http"
"slices"

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

plugins_common "github.com/jfrog/jfrog-cli-core/v2/plugins/common"
Expand All @@ -28,11 +30,23 @@ type deployRequest struct {
Version *model.Version `json:"version,omitempty"`
}

type deployCommandHandler struct {
ctx *components.Context
manifest *model.Manifest
actionMeta *model.ActionMetadata
version *model.Version
serverURL string
token string
encodeSourceCodeInBase64 bool
outputFormat format.OutputFormat
}

func GetDeployCommand() components.Command {
return components.Command{
Name: "deploy",
Description: "Deploy a worker",
Aliases: []string{"d"},
Name: "deploy",
Description: "Deploy a worker",
Aliases: []string{"d"},
SupportedFormats: []format.OutputFormat{format.Json},
Flags: []components.Flag{
plugins_common.GetServerIdFlag(),
model.GetTimeoutFlag(),
Expand All @@ -43,6 +57,15 @@ func GetDeployCommand() components.Command {
model.GetBase64Flag(),
},
Action: func(c *components.Context) error {
var outputFormat format.OutputFormat
if slices.Contains(c.FlagsUsed, format.FlagName) {
var fmtErr error
outputFormat, fmtErr = c.GetOutputFormat()
if fmtErr != nil {
return fmtErr
}
}

server, err := model.GetServerDetails(c)
if err != nil {
return err
Expand Down Expand Up @@ -102,18 +125,28 @@ func GetDeployCommand() components.Command {
} else {
encodeSourceCodeInBase64 = *options.ShouldEncodeSourceCodeInBase64 || c.GetBoolFlagValue(model.FlagBase64)
}
return runDeployCommand(c, manifest, actionMeta, version, server.GetUrl(), server.GetAccessToken(), encodeSourceCodeInBase64)

return (&deployCommandHandler{
ctx: c,
manifest: manifest,
actionMeta: actionMeta,
version: version,
serverURL: server.GetUrl(),
token: server.GetAccessToken(),
encodeSourceCodeInBase64: encodeSourceCodeInBase64,
outputFormat: outputFormat,
}).run()
},
}
}

func runDeployCommand(ctx *components.Context, manifest *model.Manifest, actionMeta *model.ActionMetadata, version *model.Version, serverURL string, token string, encodeSourceCodeInBase64 bool) error {
existingWorker, err := common.FetchWorkerDetails(ctx, serverURL, token, manifest.Name, manifest.ProjectKey)
func (h *deployCommandHandler) run() error {
existingWorker, err := common.FetchWorkerDetails(h.ctx, h.serverURL, h.token, h.manifest.Name, h.manifest.ProjectKey)
if err != nil {
return err
}

body, err := prepareDeployRequest(ctx, manifest, actionMeta, version, existingWorker, encodeSourceCodeInBase64)
body, err := h.prepareRequest(existingWorker)
if err != nil {
return err
}
Expand All @@ -123,70 +156,81 @@ func runDeployCommand(ctx *components.Context, manifest *model.Manifest, actionM
return err
}

var responseStatus int
var contentHandler common.APIContentHandler
if h.outputFormat != format.None {
contentHandler = func(body []byte) error {
return common.PrintJSONOrStatus(responseStatus, body)
}
}

if existingWorker == nil {
log.Info(fmt.Sprintf("Deploying worker '%s'", manifest.Name))
err = common.CallWorkerAPI(ctx, common.APICallParams{
Method: http.MethodPost,
ServerURL: serverURL,
ServerToken: token,
Body: bodyBytes,
OkStatuses: []int{http.StatusCreated},
Path: []string{"workers"},
APIVersion: common.APIVersionV2,
log.Info(fmt.Sprintf("Deploying worker '%s'", h.manifest.Name))
err = common.CallWorkerAPI(h.ctx, common.APICallParams{
Method: http.MethodPost,
ServerURL: h.serverURL,
ServerToken: h.token,
Body: bodyBytes,
OkStatuses: []int{http.StatusCreated},
Path: []string{"workers"},
APIVersion: common.APIVersionV2,
OnContent: contentHandler,
CaptureStatus: &responseStatus,
})
if err == nil {
log.Info(fmt.Sprintf("Worker '%s' deployed", manifest.Name))
log.Info(fmt.Sprintf("Worker '%s' deployed", h.manifest.Name))
}
} else {
log.Info(fmt.Sprintf("Updating worker '%s'", h.manifest.Name))
err = common.CallWorkerAPI(h.ctx, common.APICallParams{
Method: http.MethodPut,
ServerURL: h.serverURL,
ServerToken: h.token,
Body: bodyBytes,
OkStatuses: []int{http.StatusNoContent},
Path: []string{"workers"},
APIVersion: common.APIVersionV2,
OnContent: contentHandler,
CaptureStatus: &responseStatus,
})
if err == nil {
log.Info(fmt.Sprintf("Worker '%s' updated", h.manifest.Name))
}
return err
}

log.Info(fmt.Sprintf("Updating worker '%s'", manifest.Name))
err = common.CallWorkerAPI(ctx, common.APICallParams{
Method: http.MethodPut,
ServerURL: serverURL,
ServerToken: token,
Body: bodyBytes,
OkStatuses: []int{http.StatusNoContent},
Path: []string{"workers"},
APIVersion: common.APIVersionV2,
})
if err == nil {
log.Info(fmt.Sprintf("Worker '%s' updated", manifest.Name))
}

return err
}

func prepareDeployRequest(ctx *components.Context, manifest *model.Manifest, actionMeta *model.ActionMetadata, version *model.Version, existingWorker *model.WorkerDetails, encodeSourceCodeInBase64 bool) (*deployRequest, error) {
sourceCode, err := common.ReadSourceCode(manifest)
func (h *deployCommandHandler) prepareRequest(existingWorker *model.WorkerDetails) (*deployRequest, error) {
sourceCode, err := common.ReadSourceCode(h.manifest)
if err != nil {
return nil, err
}
sourceCode = common.CleanImports(sourceCode)

if encodeSourceCodeInBase64 {
if h.encodeSourceCodeInBase64 {
sourceCode = "base64:" + base64.StdEncoding.EncodeToString([]byte(sourceCode))
}

var secrets []*model.Secret

if !ctx.GetBoolFlagValue(model.FlagNoSecrets) {
secrets = common.PrepareSecretsUpdate(manifest, existingWorker)
if !h.ctx.GetBoolFlagValue(model.FlagNoSecrets) {
secrets = common.PrepareSecretsUpdate(h.manifest, existingWorker)
}

payload := &deployRequest{
Key: manifest.Name,
Action: actionMeta.Action,
Description: manifest.Description,
Enabled: manifest.Enabled,
Debug: manifest.Debug,
Key: h.manifest.Name,
Action: h.actionMeta.Action,
Description: h.manifest.Description,
Enabled: h.manifest.Enabled,
Debug: h.manifest.Debug,
SourceCode: sourceCode,
Secrets: secrets,
ProjectKey: manifest.ProjectKey,
Version: version,
ProjectKey: h.manifest.ProjectKey,
Version: h.version,
}

if actionMeta.MandatoryFilter {
payload.FilterCriteria = manifest.FilterCriteria
if h.actionMeta.MandatoryFilter {
payload.FilterCriteria = h.manifest.FilterCriteria
}
return payload, nil
}
49 changes: 49 additions & 0 deletions commands/deploy_cmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@
package commands

import (
"bytes"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"os"
"testing"
"time"

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

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -344,3 +347,49 @@ func getExpectedDeployRequestForAction(

return r
}

func setupDeployFormatTest(t *testing.T) (func(args ...string) error, *bytes.Buffer) {
t.Helper()

serverStub := common.NewServerStub(t).
WithDefaultActionsMetadataEndpoint().
WithGetOneEndpoint().
WithOptionsEndpoint().
WithCreateEndpoint(nil)
common.NewMockWorkerServer(t, serverStub)

runCmd := common.CreateCliRunner(t, GetInitCommand(), GetDeployCommand())

_, workerName := common.PrepareWorkerDirForTest(t)
require.NoError(t, runCmd("worker", "init", "BEFORE_UPLOAD", workerName))

var out bytes.Buffer
common.SetCliOut(&out)
t.Cleanup(func() { common.SetCliOut(os.Stdout) })

return runCmd, &out
}

func TestWorkerDeploy_FormatJSON(t *testing.T) {
runCmd, out := setupDeployFormatTest(t)

require.NoError(t, runCmd("worker", "deploy", "--"+format.FlagName, "json"))
assert.True(t, json.Valid(out.Bytes()), "expected valid JSON output, got: %s", out.String())
assert.Contains(t, out.String(), "status_code")
assert.Contains(t, out.String(), "content")
}

func TestWorkerDeploy_FormatTableRejected(t *testing.T) {
runCmd, _ := setupDeployFormatTest(t)

err := runCmd("worker", "deploy", "--"+format.FlagName, "table")
require.Error(t, err)
assert.Contains(t, err.Error(), "only the following output formats are supported")
}

func TestWorkerDeploy_NoFormat(t *testing.T) {
runCmd, out := setupDeployFormatTest(t)

require.NoError(t, runCmd("worker", "deploy"))
assert.Empty(t, out.String(), "expected no JSON output when --format is not set, got: %s", out.String())
}
Loading
Loading