From 81f34a71e804d2b7af9d4699b940586f7477eb41 Mon Sep 17 00:00:00 2001 From: Yuri Novo Date: Sun, 16 Aug 2026 09:29:30 +0300 Subject: [PATCH] APP-6261 - Add-aql-resource-type-for-version-create --- .../version/create_app_version_cmd_test.go | 87 +++++++++++++++++++ .../version/testfiles/all-sources-spec.json | 5 +- .../commands/version/testfiles/aql-spec.json | 5 ++ .../testfiles/aql-with-filters-spec.json | 13 +++ .../update_app_version_sources_cmd_test.go | 15 ++++ .../commands/version/version_source_parser.go | 24 ++++- apptrust/model/create_app_version_request.go | 1 + e2e/format_test.go | 3 +- e2e/utils/e2e_utils.go | 18 ++-- e2e/version_test.go | 85 +++++++++++++++++- 10 files changed, 243 insertions(+), 13 deletions(-) create mode 100644 apptrust/commands/version/testfiles/aql-spec.json create mode 100644 apptrust/commands/version/testfiles/aql-with-filters-spec.json diff --git a/apptrust/commands/version/create_app_version_cmd_test.go b/apptrust/commands/version/create_app_version_cmd_test.go index 8ab84e0..84b3c43 100644 --- a/apptrust/commands/version/create_app_version_cmd_test.go +++ b/apptrust/commands/version/create_app_version_cmd_test.go @@ -1,6 +1,7 @@ package version import ( + "encoding/json" "errors" "testing" @@ -575,6 +576,59 @@ func TestParseArtifacts(t *testing.T) { } } +func TestParseAQL(t *testing.T) { + tests := []struct { + name string + spec *versionSpec + expected string + }{ + { + name: "items_find object", + spec: &versionSpec{ + AQL: &aqlSpec{ItemsFind: json.RawMessage(`{"repo":"my-repo"}`)}, + }, + expected: `items.find({"repo":"my-repo"})`, + }, + { + name: "nested items_find object", + spec: &versionSpec{ + AQL: &aqlSpec{ItemsFind: json.RawMessage(`{"repo":"my-repo","$or":[{"type":"file"}]}`)}, + }, + expected: `items.find({"repo":"my-repo","$or":[{"type":"file"}]})`, + }, + { + name: "aql absent", + spec: &versionSpec{}, + expected: "", + }, + { + name: "aql present with empty items_find", + spec: &versionSpec{ + AQL: &aqlSpec{ItemsFind: json.RawMessage(``)}, + }, + expected: "", + }, + { + name: "aql present with null items_find", + spec: &versionSpec{ + AQL: &aqlSpec{ItemsFind: json.RawMessage(`null`)}, + }, + expected: "", + }, + { + name: "spec nil aql", + spec: &versionSpec{AQL: nil}, + expected: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.expected, parseAQL(tt.spec)) + }) + } +} + func TestCreateAppVersionCommand_SpecFileSuite(t *testing.T) { tests := []struct { name string @@ -724,6 +778,38 @@ func TestCreateAppVersionCommand_SpecFileSuite(t *testing.T) { Version: "4.5.6", }, }, + AQL: `items.find({"repo":"my-repo","type":"file"})`, + }, + }, + }, + { + name: "aql spec file", + specPath: "./testfiles/aql-spec.json", + args: []string{"app-aql", "1.0.0"}, + expectsPayload: &model.CreateAppVersionRequest{ + ApplicationKey: "app-aql", + Version: "1.0.0", + Draft: false, + Sources: &model.CreateVersionSources{ + AQL: `items.find({"repo":"my-repo"})`, + }, + }, + }, + { + name: "aql with filters spec file", + specPath: "./testfiles/aql-with-filters-spec.json", + args: []string{"app-aql-filters", "1.0.0"}, + expectsPayload: &model.CreateAppVersionRequest{ + ApplicationKey: "app-aql-filters", + Version: "1.0.0", + Draft: false, + Sources: &model.CreateVersionSources{ + AQL: `items.find({"repo":"my-repo"})`, + }, + Filters: &model.CreateVersionFilters{ + Included: []*model.CreateVersionSourceFilter{ + {PackageType: "docker", PackageName: "frontend-*"}, + }, }, }, }, @@ -822,6 +908,7 @@ func TestCreateAppVersionCommand_SpecFileSuite(t *testing.T) { Version: "4.5.6", }, }, + AQL: `items.find({"repo":"my-repo","type":"file"})`, }, }, }, diff --git a/apptrust/commands/version/testfiles/all-sources-spec.json b/apptrust/commands/version/testfiles/all-sources-spec.json index 47b7b26..9143b54 100644 --- a/apptrust/commands/version/testfiles/all-sources-spec.json +++ b/apptrust/commands/version/testfiles/all-sources-spec.json @@ -56,5 +56,8 @@ "application_key": "dependency-app-2", "version": "4.5.6" } - ] + ], + "aql": { + "items.find": {"repo":"my-repo","type":"file"} + } } diff --git a/apptrust/commands/version/testfiles/aql-spec.json b/apptrust/commands/version/testfiles/aql-spec.json new file mode 100644 index 0000000..c757394 --- /dev/null +++ b/apptrust/commands/version/testfiles/aql-spec.json @@ -0,0 +1,5 @@ +{ + "aql": { + "items.find": {"repo":"my-repo"} + } +} diff --git a/apptrust/commands/version/testfiles/aql-with-filters-spec.json b/apptrust/commands/version/testfiles/aql-with-filters-spec.json new file mode 100644 index 0000000..e2f6faf --- /dev/null +++ b/apptrust/commands/version/testfiles/aql-with-filters-spec.json @@ -0,0 +1,13 @@ +{ + "aql": { + "items.find": {"repo":"my-repo"} + }, + "filters": { + "included": [ + { + "package_type": "docker", + "package_name": "frontend-*" + } + ] + } +} diff --git a/apptrust/commands/version/update_app_version_sources_cmd_test.go b/apptrust/commands/version/update_app_version_sources_cmd_test.go index b8e75d0..f752333 100644 --- a/apptrust/commands/version/update_app_version_sources_cmd_test.go +++ b/apptrust/commands/version/update_app_version_sources_cmd_test.go @@ -193,6 +193,21 @@ func TestUpdateAppVersionSourcesCommand_SourceFlagsSuite(t *testing.T) { expectsDryRun: false, expectsFailFast: true, }, + { + name: "update with aql spec file", + ctxSetup: func(ctx *components.Context) { + ctx.Arguments = []string{"app-key", "1.0.0"} + ctx.AddStringFlag(commands.SpecFlag, "./testfiles/aql-spec.json") + }, + expectsPayload: &model.UpdateVersionSourcesRequest{ + AddSources: &model.CreateVersionSources{ + AQL: `items.find({"repo":"my-repo"})`, + }, + }, + expectsSync: true, + expectsDryRun: false, + expectsFailFast: true, + }, { name: "update with spec file and spec-vars", ctxSetup: func(ctx *components.Context) { diff --git a/apptrust/commands/version/version_source_parser.go b/apptrust/commands/version/version_source_parser.go index 6be66eb..a882c90 100644 --- a/apptrust/commands/version/version_source_parser.go +++ b/apptrust/commands/version/version_source_parser.go @@ -2,6 +2,7 @@ package version import ( "encoding/json" + "fmt" "strconv" "strings" @@ -21,6 +22,11 @@ type versionSpec struct { ReleaseBundles []model.CreateVersionReleaseBundle `json:"release_bundles,omitempty"` Versions []model.CreateVersionReference `json:"versions,omitempty"` Filters *model.CreateVersionFilters `json:"filters,omitempty"` + AQL *aqlSpec `json:"aql,omitempty"` +} + +type aqlSpec struct { + ItemsFind json.RawMessage `json:"items.find,omitempty"` } // validateNoSpecAndFlagsTogether returns error if both --spec and any other source flag or filter flag are set. @@ -154,9 +160,11 @@ func loadSourcesFromSpec(ctx *components.Context) (*model.CreateVersionSources, return nil, nil, err } + aql := parseAQL(spec) + // Validation: if all sources are empty, return error - if (len(spec.Packages) == 0) && (len(spec.Builds) == 0) && (len(spec.ReleaseBundles) == 0) && (len(spec.Versions) == 0) && (len(spec.Artifacts) == 0) { - return nil, nil, errorutils.CheckErrorf("Spec file is empty: must provide at least one source (artifacts, packages, builds, release_bundles, or versions)") + if len(spec.Packages) == 0 && len(spec.Builds) == 0 && len(spec.ReleaseBundles) == 0 && len(spec.Versions) == 0 && len(spec.Artifacts) == 0 && aql == "" { + return nil, nil, errorutils.CheckErrorf("Spec file is empty: must provide at least one source (artifacts, packages, builds, release_bundles, versions, or aql)") } sources := &model.CreateVersionSources{ @@ -165,11 +173,23 @@ func loadSourcesFromSpec(ctx *components.Context) (*model.CreateVersionSources, Builds: spec.Builds, ReleaseBundles: spec.ReleaseBundles, Versions: spec.Versions, + AQL: aql, } return sources, spec.Filters, nil } +func parseAQL(spec *versionSpec) string { + if spec == nil || spec.AQL == nil { + return "" + } + itemsFind := string(spec.AQL.ItemsFind) + if itemsFind == "" || itemsFind == "null" { + return "" + } + return fmt.Sprintf("items.find(%s)", itemsFind) +} + func parseBuilds(buildsStr string) ([]model.CreateVersionBuild, error) { const ( nameField = "name" diff --git a/apptrust/model/create_app_version_request.go b/apptrust/model/create_app_version_request.go index 22f2b22..d282076 100644 --- a/apptrust/model/create_app_version_request.go +++ b/apptrust/model/create_app_version_request.go @@ -33,6 +33,7 @@ type CreateVersionSources struct { Builds []CreateVersionBuild `json:"builds,omitempty"` ReleaseBundles []CreateVersionReleaseBundle `json:"release_bundles,omitempty"` Versions []CreateVersionReference `json:"versions,omitempty"` + AQL string `json:"aql,omitempty"` } type CreateVersionSourceFilter struct { diff --git a/e2e/format_test.go b/e2e/format_test.go index 947637d..e3c078a 100644 --- a/e2e/format_test.go +++ b/e2e/format_test.go @@ -139,7 +139,8 @@ func TestVersionUpdate_OutputFormat(t *testing.T) { func TestVersionUpdateSources_OutputFormat(t *testing.T) { testPackage := utils.GetTestPackage(t) - artifactPath := utils.GetTestArtifact(t) + artifactRepo, artifactFile := utils.GetTestArtifact(t) + artifactPath := artifactRepo + "/" + artifactFile prepareDraftVersion := func(t *testing.T, suffix string) (appKey, version string, cleanup func()) { appKey = utils.GenerateUniqueKey("version-upd-src-fmt-" + suffix) diff --git a/e2e/utils/e2e_utils.go b/e2e/utils/e2e_utils.go index 00a4948..2d2f726 100644 --- a/e2e/utils/e2e_utils.go +++ b/e2e/utils/e2e_utils.go @@ -37,9 +37,10 @@ var ( AppTrustCli *coreTests.JfrogCli - testProjectKey string - testPackageRes *TestPackageResources - testArtifactPath string + testProjectKey string + testPackageRes *TestPackageResources + testArtifactRepoKey string + testArtifactFileName string ) func LoadCredentials() string { @@ -81,12 +82,13 @@ func GetTestPackage(t *testing.T) *TestPackageResources { return testPackageRes } -func GetTestArtifact(t *testing.T) string { - if testArtifactPath == "" { - repoKey := createGenericRepo(t) - testArtifactPath = UploadTestArtifact(t, repoKey, "test-artifact.txt") +func GetTestArtifact(t *testing.T) (repoKey, fileName string) { + if testArtifactRepoKey == "" { + testArtifactRepoKey = createGenericRepo(t) + testArtifactFileName = "test-artifact.txt" + UploadTestArtifact(t, testArtifactRepoKey, testArtifactFileName) } - return testArtifactPath + return testArtifactRepoKey, testArtifactFileName } func GenerateUniqueKey(prefix string) string { diff --git a/e2e/version_test.go b/e2e/version_test.go index f79ecf4..77ae38f 100644 --- a/e2e/version_test.go +++ b/e2e/version_test.go @@ -6,6 +6,8 @@ import ( "encoding/json" "fmt" "net/http" + "os" + "path/filepath" "strings" "testing" "time" @@ -149,6 +151,86 @@ func TestCreateVersion_ReleaseBundle(t *testing.T) { assertVersionContent(t, testPackage, versionContent, statusCode, appKey, version) } +func TestCreateVersion_AQL(t *testing.T) { + appKey := utils.GenerateUniqueKey("app-version-create-aql") + utils.CreateBasicApplication(t, appKey) + defer utils.DeleteApplication(t, appKey) + + repoKey, fileName := utils.GetTestArtifact(t) + artifactPath := repoKey + "/" + fileName + version := "1.0.13" + + specPath := writeAQLSpec(t, fmt.Sprintf(`{"repo":"%s","name":"%s"}`, repoKey, fileName), "") + + err := utils.AppTrustCli.Exec("version-create", appKey, version, "--spec="+specPath) + require.NoError(t, err) + defer utils.DeleteApplicationVersion(t, appKey, version) + + versionContent, statusCode, err := utils.GetApplicationVersion(appKey, version) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, statusCode) + require.NotNil(t, versionContent) + assert.Equal(t, appKey, versionContent.ApplicationKey) + assert.Equal(t, version, versionContent.Version) + assert.Equal(t, utils.StatusCompleted, versionContent.Status) + assert.True(t, containsArtifactPath(versionContent, artifactPath), + "expected artifact %q resolved by AQL to appear in releasables", artifactPath) +} + +func TestCreateVersion_AQL_WithExcludeFilter(t *testing.T) { + appKey := utils.GenerateUniqueKey("app-version-create-aql-filters") + utils.CreateBasicApplication(t, appKey) + defer utils.DeleteApplication(t, appKey) + + repoKey := utils.CreateGenericRepoWithEnv(t, utils.GenerateUniqueKey("aql-filter"), nil) + includedPath := utils.UploadTestArtifact(t, repoKey, "included-artifact.txt") + excludedPath := utils.UploadTestArtifact(t, repoKey, "excluded-artifact.txt") + version := "1.0.14" + + itemsFindJSON := fmt.Sprintf(`{"repo":"%s"}`, repoKey) + filtersJSON := fmt.Sprintf(`"filters":{"excluded":[{"path":"%s"}]}`, excludedPath) + specPath := writeAQLSpec(t, itemsFindJSON, filtersJSON) + + err := utils.AppTrustCli.Exec("version-create", appKey, version, "--spec="+specPath) + require.NoError(t, err) + defer utils.DeleteApplicationVersion(t, appKey, version) + + versionContent, statusCode, err := utils.GetApplicationVersion(appKey, version) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, statusCode) + require.NotNil(t, versionContent) + assert.Equal(t, appKey, versionContent.ApplicationKey) + assert.Equal(t, version, versionContent.Version) + assert.Equal(t, utils.StatusCompleted, versionContent.Status) + assert.True(t, containsArtifactPath(versionContent, includedPath), + "expected included artifact %q to remain after filter", includedPath) + assert.False(t, containsArtifactPath(versionContent, excludedPath), + "expected excluded artifact %q to be filtered out", excludedPath) +} + +func writeAQLSpec(t *testing.T, itemsFindJSON, extraTopLevelJSON string) string { + t.Helper() + body := fmt.Sprintf(`{"aql":{"items.find":%s}`, itemsFindJSON) + if extraTopLevelJSON != "" { + body += "," + extraTopLevelJSON + } + body += "}" + path := filepath.Join(t.TempDir(), "aql-spec.json") + require.NoError(t, os.WriteFile(path, []byte(body), 0o600)) + return path +} + +func containsArtifactPath(vc *utils.VersionContentResponse, target string) bool { + for _, r := range vc.Releasables { + for _, a := range r.Artifacts { + if strings.Contains(target, a.Path) || strings.Contains(a.Path, target) { + return true + } + } + } + return false +} + func TestCreateVersion_Build(t *testing.T) { // Prepare appKey := utils.GenerateUniqueKey("app-version-create-build") @@ -378,7 +460,8 @@ func TestUpdateDraftVersionSources(t *testing.T) { err := utils.AppTrustCli.Exec("version-create", appKey, version, packageFlag, "--draft") require.NoError(t, err) defer utils.DeleteApplicationVersion(t, appKey, version) - artifactPath := utils.GetTestArtifact(t) + artifactRepo, artifactFile := utils.GetTestArtifact(t) + artifactPath := artifactRepo + "/" + artifactFile artifactFlag := fmt.Sprintf("--source-type-artifacts=path=%s", artifactPath) err = utils.AppTrustCli.Exec("version-update-sources", appKey, version, artifactFlag)