Skip to content

Commit

Permalink
Add scheduled event support (#13)
Browse files Browse the repository at this point in the history
  • Loading branch information
ehl-jf authored Jan 2, 2025
1 parent 87060da commit 32c4e60
Show file tree
Hide file tree
Showing 32 changed files with 1,068 additions and 135 deletions.
48 changes: 48 additions & 0 deletions .github/scripts/gentz.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#!/usr/bin/env bash

set -e # Exit on any error

fail() {
echo "Error: $1" >&2
exit 1
}

# Get GOROOT using `go env`
GOROOT=$(go env GOROOT)
if [ $? -ne 0 ]; then
fail "Failed to get GOROOT"
fi

# Get the path to the zoneinfo.zip
ZIPNAME="$GOROOT/lib/time/zoneinfo.zip"
if [ ! -f "$ZIPNAME" ]; then
fail "zoneinfo.zip not found at $ZIPNAME"
fi

# Open the output file
GOFILE=${GOFILE:-"timezones.go"}
GOFILE="$(basename "$GOFILE" ".go")_generated.go"
if ! touch "$GOFILE"; then
fail "Failed to create output file $GOFILE"
fi

# Write the generated code header
{
echo "// Code generated by \"$(basename "${BASH_SOURCE[0]}")\""
echo ""
echo "package ${GOPACKAGE:-main}"
echo ""
echo "var TimeZones = []string{"
} > "$GOFILE"

# List the contents of the zip file and append them to the output file
if ! unzip -Z1 "$ZIPNAME" | while read -r path; do
echo " \"${path//Etc\//}\"," >> "$GOFILE"
done; then
fail "Failed to process zip file $ZIPNAME"
fi

# Close the Go array
echo "}" >> "$GOFILE"

echo "TimeZones list generated successfully in $GOFILE."
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ jobs:
XUNIT_OUTFILE: test-reports/unit.xml
run: |
test -d "$(dirname $XUNIT_OUTFILE)" || mkdir -p "$(dirname $XUNIT_OUTFILE)"
.github/scripts/gotest.sh ./...
make test
- name: Publish Results
uses: EnricoMi/publish-unit-test-result-action@v2
Expand Down
23 changes: 7 additions & 16 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -48,32 +48,23 @@ prereq::
$(GOCMD) install github.com/jstemmer/[email protected]
GOBIN=${TOOLS_DIR} $(GOCMD) install go.uber.org/mock/[email protected]

build::
go env GOOS GOARCH
go build -ldflags="${LINKERFLAGS}" -gcflags ${COMPILERFLAGS} -o ${BINARY_CLI}/worker-cli-plugin main.go


build-install:: build
mkdir -p "${HOME}/.jfrog/plugins/worker/bin"
mv ${BINARY_CLI}/worker-cli-plugin "${HOME}/.jfrog/plugins/worker/bin/worker"
chmod +x "${HOME}/.jfrog/plugins/worker/bin/worker"

########## TEST ##########

.PHONY: clean-mock
clean-mock:
@echo Cleaning generated mock files
find . -path "*/mocks/*.go" -delete

.PHONY: generate-mock
generate-mock: clean-mock
@echo Generating test mocks
TOOLS_DIR=$(TOOLS_DIR) go generate ./...
generate: prereq clean-mock
TOOLS_DIR=$(TOOLS_DIR) SCRIPTS_DIR=$(SCRIPTS_DIR) go generate ./...

# Used in pipeline so we keep this alias
generate-mock: generate

test-prereq: prereq generate-mock
test-prereq: generate-mock
mkdir -p target/reports

test: PACKAGES=./...
test: TAGS=-tags=test
test: TEST_ARGS=-short
test: test-prereq do-run-tests

Expand Down
1 change: 1 addition & 0 deletions cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ func getWorkerNamespace() components.Namespace {
commands.GetListCommand(),
commands.GetAddSecretCommand(),
commands.GetListEventsCommand(),
commands.GetEditScheduleCommand(),
},
}
}
20 changes: 20 additions & 0 deletions commands/common/manifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (

"github.com/jfrog/jfrog-cli-platform-services/model"
"github.com/jfrog/jfrog-client-go/utils/log"
"github.com/robfig/cron/v3"
)

// ReadManifest reads a manifest from the working directory or from the directory provided as argument.
Expand Down Expand Up @@ -144,6 +145,25 @@ func DecryptManifestSecrets(mf *model.Manifest, withPassword ...string) error {
return nil
}

var cronParser = cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow)

func ValidateScheduleCriteria(c *model.ScheduleFilterCriteria) error {
if c.Cron == "" {
return errors.New("missing cron expression")
}

if _, err := cronParser.Parse(c.Cron); err != nil {
log.Debug(fmt.Sprintf("invalid cron expression: %+v", err))
return errors.New("invalid cron expression")
}

if c.Timezone != "" && !model.IsValidTimezone(c.Timezone) {
return errors.New("invalid timezone '" + c.Timezone + "'")
}

return nil
}

func invalidManifestErr(reason string) error {
return fmt.Errorf("invalid manifest: %s", reason)
}
54 changes: 53 additions & 1 deletion commands/common/manifest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ package common

import (
"encoding/json"
"errors"
"os"
"path/filepath"
"regexp"
"testing"

"github.com/jfrog/jfrog-cli-platform-services/model"
Expand Down Expand Up @@ -212,7 +214,7 @@ func TestManifest_Validate(t *testing.T) {
mf.Action = "HACK_ME"
}),
assert: func(t *testing.T, err error) {
assert.EqualError(t, err, invalidManifestErr("action 'HACK_ME' not found").Error())
assert.Regexp(t, regexp.MustCompile("action 'HACK_ME' not found"), err)
},
},
{
Expand Down Expand Up @@ -291,6 +293,56 @@ func TestManifest_DecryptSecrets(t *testing.T) {
}
}

func TestManifest_ValidateScheduleCriteria(t *testing.T) {
tests := []struct {
name string
criteria *model.ScheduleFilterCriteria
wantErr error
}{
{
name: "valid",
criteria: &model.ScheduleFilterCriteria{
Cron: "0 1 1 * *",
Timezone: "UTC",
},
},
{
name: "missing cron",
criteria: &model.ScheduleFilterCriteria{
Timezone: "UTC",
},
wantErr: errors.New("missing cron expression"),
},
{
name: "invalid cron",
criteria: &model.ScheduleFilterCriteria{
Cron: "0 0 0 * * * *",
Timezone: "UTC",
},
wantErr: errors.New("invalid cron expression"),
},
{
name: "invalid timezone",
criteria: &model.ScheduleFilterCriteria{
Cron: "0 1 1 * *",
Timezone: "America/Toulouse",
},
wantErr: errors.New("invalid timezone 'America/Toulouse'"),
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidateScheduleCriteria(tt.criteria)
if tt.wantErr == nil {
assert.NoError(t, err)
} else {
assert.EqualError(t, err, tt.wantErr.Error())
}
})
}
}

func patchedManifestSample(patch func(mf *model.Manifest)) *model.Manifest {
patched := *manifestSample
patch(&patched)
Expand Down
1 change: 1 addition & 0 deletions commands/common/test_commons.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ func GenerateFromSamples(t require.TestingT, templates embed.FS, action string,
"Application": actionMeta.Action.Application,
"WorkerName": workerName,
"HasRepoFilterCriteria": actionMeta.MandatoryFilter && actionMeta.FilterType == model.FilterTypeRepo,
"HasSchedule": actionMeta.MandatoryFilter && actionMeta.FilterType == model.FilterTypeSchedule,
"HasTests": len(skipTests) == 0 || !skipTests[0],
"HasRequestType": actionMeta.ExecutionRequestType != "",
"ExecutionRequestType": actionMeta.ExecutionRequestType,
Expand Down
4 changes: 3 additions & 1 deletion commands/common/test_worker_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -361,7 +361,9 @@ func (s *ServerStub) handleGetAllMetadata(metadata ActionsMetadata) http.Handler
res.Header().Set("Content-Type", "application/json")

_, err := res.Write([]byte(MustJsonMarshal(s.test, metadata)))
require.NoError(s.test, err)
if err != nil {
s.test.Logf("Failed to write response: %v", err)
}
}
}

Expand Down
11 changes: 11 additions & 0 deletions commands/deploy_cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,17 @@ func GetDeployCommand() components.Command {
return err
}

actionMeta, err := actionsMeta.FindAction(manifest.Action, manifest.Application)
if err != nil {
return err
}

if actionMeta.MandatoryFilter && actionMeta.FilterType == model.FilterTypeSchedule {
if err = common.ValidateScheduleCriteria(&manifest.FilterCriteria.Schedule); err != nil {
return fmt.Errorf("manifest validation failed: %w", err)
}
}

if !c.GetBoolFlagValue(model.FlagNoSecrets) {
if err = common.DecryptManifestSecrets(manifest); err != nil {
return err
Expand Down
15 changes: 14 additions & 1 deletion commands/deploy_cmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,19 @@ func TestDeployCommand(t *testing.T) {
mf.ProjectKey = "proj-1"
},
},
{
name: "should validate schedule",
workerAction: "SCHEDULED_EVENT",
workerName: "wk-3",
serverBehavior: common.NewServerStub(t).WithGetOneEndpoint(),
patchManifest: func(mf *model.Manifest) {
mf.FilterCriteria.Schedule = model.ScheduleFilterCriteria{
Cron: "1h",
Timezone: "Asia/New_York",
}
},
wantErr: errors.New("manifest validation failed: invalid cron expression"),
},
{
name: "fails if timeout exceeds",
commandArgs: []string{"--" + model.FlagTimeout, "500"},
Expand Down Expand Up @@ -155,7 +168,7 @@ func TestDeployCommand(t *testing.T) {
if tt.wantErr == nil {
assert.NoError(t, err)
} else {
assert.EqualError(t, tt.wantErr, err.Error())
assert.EqualError(t, err, tt.wantErr.Error())
}
})
}
Expand Down
30 changes: 17 additions & 13 deletions commands/dry_run_cmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,14 @@ import (

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

"github.com/stretchr/testify/require"

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

func TestDryRun(t *testing.T) {
tests := []struct {
name string
commandArgs []string
initExtraArgs []string
assert common.AssertOutputFunc
patchManifest func(mf *model.Manifest)
// Use this workerKey instead of a random generated one
Expand Down Expand Up @@ -47,7 +46,7 @@ func TestDryRun(t *testing.T) {
WithToken("invalid-token").
WithTestEndpoint(nil, nil),
commandArgs: []string{`{}`},
assert: common.AssertOutputErrorRegexp(`command\s.+returned\san\sunexpected\sstatus\scode\s403`),
assert: common.AssertOutputErrorRegexp(`command.*returned\san\sunexpected\sstatus\scode\s403`),
},
{
name: "reads from stdin",
Expand Down Expand Up @@ -94,7 +93,7 @@ func TestDryRun(t *testing.T) {
{
name: "fails if timeout exceeds",
commandArgs: []string{"--" + model.FlagTimeout, "500", `{}`},
serverStub: common.NewServerStub(t).WithDelay(5*time.Second).WithTestEndpoint(nil, nil),
serverStub: common.NewServerStub(t).WithDelay(2*time.Second).WithTestEndpoint(nil, nil),
assert: common.AssertOutputError("request timed out after 500ms"),
},
{
Expand All @@ -108,8 +107,9 @@ func TestDryRun(t *testing.T) {
assert: common.AssertOutputError("missing file path"),
},
{
name: "should propagate projectKey",
workerKey: "my-worker",
name: "should propagate projectKey",
workerKey: "my-worker",
initExtraArgs: []string{"--" + model.FlagProjectKey, "my-project"},
serverStub: common.NewServerStub(t).
WithProjectKey("my-project").
WithTestEndpoint(
Expand All @@ -136,13 +136,6 @@ func TestDryRun(t *testing.T) {
workerName = tt.workerKey
}

err := runCmd("worker", "init", "BEFORE_DOWNLOAD", workerName)
require.NoError(t, err)

if tt.patchManifest != nil {
common.PatchManifest(t, tt.patchManifest)
}

if tt.serverStub == nil {
tt.serverStub = common.NewServerStub(t)
}
Expand All @@ -157,6 +150,17 @@ func TestDryRun(t *testing.T) {
}),
)

initCmd := append([]string{"worker", "init"}, tt.initExtraArgs...)
err := runCmd(append(initCmd, "BEFORE_DOWNLOAD", workerName)...)
if err != nil {
tt.assert(t, nil, err)
return
}

if tt.patchManifest != nil {
common.PatchManifest(t, tt.patchManifest)
}

if tt.stdInput != "" {
common.SetCliIn(bytes.NewReader([]byte(tt.stdInput)))
t.Cleanup(func() {
Expand Down
Loading

0 comments on commit 32c4e60

Please sign in to comment.