diff --git a/apptrust/commands/flags.go b/apptrust/commands/flags.go index 7a2f6fa..f4e3a12 100644 --- a/apptrust/commands/flags.go +++ b/apptrust/commands/flags.go @@ -49,6 +49,8 @@ const ( SourceTypeBuildsFlag = "source-type-builds" SourceTypeReleaseBundlesFlag = "source-type-release-bundles" SourceTypeApplicationVersionsFlag = "source-type-application-versions" + SourceTypePackagesFlag = "source-type-packages" + SourceTypeArtifactsFlag = "source-type-artifacts" PropertiesFlag = "properties" DeletePropertiesFlag = "delete-properties" ) @@ -82,6 +84,8 @@ var flagsMap = map[string]components.Flag{ SourceTypeBuildsFlag: components.NewStringFlag(SourceTypeBuildsFlag, "List of semicolon-separated (;) builds in the form of 'name=buildName1, id=runID1[, include-deps=true]; name=buildName2, id=runID2[, include-deps=true]' to be included in the new version.", func(f *components.StringFlag) { f.Mandatory = false }), SourceTypeReleaseBundlesFlag: components.NewStringFlag(SourceTypeReleaseBundlesFlag, "List of semicolon-separated (;) release bundles in the form of 'name=releaseBundleName1, version=version1[, project-key=project1][, repo-key=repo1]; name=releaseBundleName2, version=version2[, project-key=project2][, repo-key=repo2]' to be included in the new version.", func(f *components.StringFlag) { f.Mandatory = false }), SourceTypeApplicationVersionsFlag: components.NewStringFlag(SourceTypeApplicationVersionsFlag, "List of semicolon-separated (;) application versions in the form of 'application-key=app1, version=version1; application-key=app2, version=version2' to be included in the new version.", func(f *components.StringFlag) { f.Mandatory = false }), + SourceTypePackagesFlag: components.NewStringFlag(SourceTypePackagesFlag, "List of semicolon-separated (;) packages in the form of 'type=packageType1, name=packageName1, version=version1, repo-key=repo1; type=packageType2, name=packageName2, version=version2, repo-key=repo2' to be included in the new version.", func(f *components.StringFlag) { f.Mandatory = false }), + SourceTypeArtifactsFlag: components.NewStringFlag(SourceTypeArtifactsFlag, "List of semicolon-separated (;) artifacts in the form of 'path=repo/path/to/artifact1[, sha256=hash1]; path=repo/path/to/artifact2[, sha256=hash2]' to be included in the new version.", func(f *components.StringFlag) { f.Mandatory = false }), PropertiesFlag: components.NewStringFlag(PropertiesFlag, "Sets or updates custom properties for the application version in format 'key1=value1[,value2,...];key2=value3[,value4,...]'", func(f *components.StringFlag) { f.Mandatory = false }), DeletePropertiesFlag: components.NewStringFlag(DeletePropertiesFlag, "Remove a property key and all its values", func(f *components.StringFlag) { f.Mandatory = false }), } @@ -96,6 +100,8 @@ var commandFlags = map[string][]string{ SourceTypeBuildsFlag, SourceTypeReleaseBundlesFlag, SourceTypeApplicationVersionsFlag, + SourceTypePackagesFlag, + SourceTypeArtifactsFlag, SpecFlag, SpecVarsFlag, }, diff --git a/apptrust/commands/version/create_app_version_cmd.go b/apptrust/commands/version/create_app_version_cmd.go index 97d54f1..dcd6ef1 100644 --- a/apptrust/commands/version/create_app_version_cmd.go +++ b/apptrust/commands/version/create_app_version_cmd.go @@ -29,6 +29,7 @@ type createAppVersionCommand struct { } type createVersionSpec struct { + Artifacts []model.CreateVersionArtifact `json:"artifacts,omitempty"` Packages []model.CreateVersionPackage `json:"packages,omitempty"` Builds []model.CreateVersionBuild `json:"builds,omitempty"` ReleaseBundles []model.CreateVersionReleaseBundle `json:"release_bundles,omitempty"` @@ -115,6 +116,20 @@ func (cv *createAppVersionCommand) buildSourcesFromFlags(ctx *components.Context } sources.Versions = sourceVersions } + if packagesStr := ctx.GetStringFlagValue(commands.SourceTypePackagesFlag); packagesStr != "" { + packages, err := cv.parsePackages(packagesStr) + if err != nil { + return nil, err + } + sources.Packages = packages + } + if artifactsStr := ctx.GetStringFlagValue(commands.SourceTypeArtifactsFlag); artifactsStr != "" { + artifacts, err := cv.parseArtifacts(artifactsStr) + if err != nil { + return nil, err + } + sources.Artifacts = artifacts + } return sources, nil } @@ -137,11 +152,12 @@ func (cv *createAppVersionCommand) loadFromSpec(ctx *components.Context) (*model } // 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) { - return nil, errorutils.CheckErrorf("Spec file is empty: must provide at least one source (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) { + return nil, errorutils.CheckErrorf("Spec file is empty: must provide at least one source (artifacts, packages, builds, release_bundles, or versions)") } sources := &model.CreateVersionSources{ + Artifacts: spec.Artifacts, Packages: spec.Packages, Builds: spec.Builds, ReleaseBundles: spec.ReleaseBundles, @@ -239,6 +255,61 @@ func (cv *createAppVersionCommand) parseSourceVersions(applicationVersionsStr st return refs, nil } +func (cv *createAppVersionCommand) parsePackages(packagesStr string) ([]model.CreateVersionPackage, error) { + const ( + typeField = "type" + nameField = "name" + versionField = "version" + repositoryField = "repo-key" + ) + + var packages []model.CreateVersionPackage + packageEntries := utils.ParseSliceFlag(packagesStr) + for _, entry := range packageEntries { + packageEntryMap, err := utils.ParseKeyValueString(entry, ",") + if err != nil { + return nil, errorutils.CheckErrorf("invalid package format: %v", err) + } + err = validateRequiredFieldsInMap(packageEntryMap, typeField, nameField, versionField, repositoryField) + if err != nil { + return nil, errorutils.CheckErrorf("invalid package format: %v", err) + } + packages = append(packages, model.CreateVersionPackage{ + Type: packageEntryMap[typeField], + Name: packageEntryMap[nameField], + Version: packageEntryMap[versionField], + Repository: packageEntryMap[repositoryField], + }) + } + return packages, nil +} + +func (cv *createAppVersionCommand) parseArtifacts(artifactsStr string) ([]model.CreateVersionArtifact, error) { + const ( + pathField = "path" + sha256Field = "sha256" + ) + + var artifacts []model.CreateVersionArtifact + artifactEntries := utils.ParseSliceFlag(artifactsStr) + for _, entry := range artifactEntries { + artifactEntryMap, err := utils.ParseKeyValueString(entry, ",") + if err != nil { + return nil, errorutils.CheckErrorf("invalid artifact format: %v", err) + } + err = validateRequiredFieldsInMap(artifactEntryMap, pathField) + if err != nil { + return nil, errorutils.CheckErrorf("invalid artifact format: %v", err) + } + artifact := model.CreateVersionArtifact{ + Path: artifactEntryMap[pathField], + SHA256: artifactEntryMap[sha256Field], + } + artifacts = append(artifacts, artifact) + } + return artifacts, nil +} + func validateCreateAppVersionContext(ctx *components.Context) error { if err := validateNoSpecAndFlagsTogether(ctx); err != nil { return err @@ -250,12 +321,14 @@ func validateCreateAppVersionContext(ctx *components.Context) error { hasSource := ctx.IsFlagSet(commands.SpecFlag) || ctx.IsFlagSet(commands.SourceTypeBuildsFlag) || ctx.IsFlagSet(commands.SourceTypeReleaseBundlesFlag) || - ctx.IsFlagSet(commands.SourceTypeApplicationVersionsFlag) + ctx.IsFlagSet(commands.SourceTypeApplicationVersionsFlag) || + ctx.IsFlagSet(commands.SourceTypePackagesFlag) || + ctx.IsFlagSet(commands.SourceTypeArtifactsFlag) if !hasSource { return errorutils.CheckErrorf( - "At least one source flag is required to create an application version. Please provide --%s or at least one of the following: --%s, --%s, --%s.", - commands.SpecFlag, commands.SourceTypeBuildsFlag, commands.SourceTypeReleaseBundlesFlag, commands.SourceTypeApplicationVersionsFlag) + "At least one source flag is required to create an application version. Please provide --%s or at least one of the following: --%s, --%s, --%s, --%s, --%s.", + commands.SpecFlag, commands.SourceTypeBuildsFlag, commands.SourceTypeReleaseBundlesFlag, commands.SourceTypeApplicationVersionsFlag, commands.SourceTypePackagesFlag, commands.SourceTypeArtifactsFlag) } return nil @@ -292,6 +365,8 @@ func validateNoSpecAndFlagsTogether(ctx *components.Context) error { commands.SourceTypeBuildsFlag, commands.SourceTypeReleaseBundlesFlag, commands.SourceTypeApplicationVersionsFlag, + commands.SourceTypePackagesFlag, + commands.SourceTypeArtifactsFlag, } for _, flag := range otherSourceFlags { if ctx.IsFlagSet(flag) { diff --git a/apptrust/commands/version/create_app_version_cmd_test.go b/apptrust/commands/version/create_app_version_cmd_test.go index 44d9cf3..ace3820 100644 --- a/apptrust/commands/version/create_app_version_cmd_test.go +++ b/apptrust/commands/version/create_app_version_cmd_test.go @@ -119,6 +119,8 @@ func TestCreateAppVersionCommand_FlagsSuite(t *testing.T) { ctx.AddStringFlag(commands.SourceTypeBuildsFlag, "name=build1,id=1.0.0,include_deps=true;name=build2,id=2.0.0,include_deps=false") ctx.AddStringFlag(commands.SourceTypeReleaseBundlesFlag, "name=rb1,version=1.0.0;name=rb2,version=2.0.0") ctx.AddStringFlag(commands.SourceTypeApplicationVersionsFlag, "application-key=source-app,version=3.2.1") + ctx.AddStringFlag(commands.SourceTypePackagesFlag, "type=npm,name=pkg1,version=1.0.0,repo-key=repo1;type=docker,name=pkg2,version=2.0.0,repo-key=repo2") + ctx.AddStringFlag(commands.SourceTypeArtifactsFlag, "path=repo/path/to/artifact1.jar,sha256=abc123;path=repo/path/to/artifact2.war") }, expectsPayload: &model.CreateAppVersionRequest{ ApplicationKey: "app-key", @@ -136,6 +138,14 @@ func TestCreateAppVersionCommand_FlagsSuite(t *testing.T) { Versions: []model.CreateVersionReference{ {ApplicationKey: "source-app", Version: "3.2.1"}, }, + Packages: []model.CreateVersionPackage{ + {Type: "npm", Name: "pkg1", Version: "1.0.0", Repository: "repo1"}, + {Type: "docker", Name: "pkg2", Version: "2.0.0", Repository: "repo2"}, + }, + Artifacts: []model.CreateVersionArtifact{ + {Path: "repo/path/to/artifact1.jar", SHA256: "abc123"}, + {Path: "repo/path/to/artifact2.war"}, + }, }, }, }, @@ -157,7 +167,7 @@ func TestCreateAppVersionCommand_FlagsSuite(t *testing.T) { }, expectsPayload: nil, expectsError: true, - errorContains: "At least one source flag is required to create an application version. Please provide --spec or at least one of the following: --source-type-builds, --source-type-release-bundles, --source-type-application-versions.", + errorContains: "At least one source flag is required to create an application version. Please provide --spec or at least one of the following: --source-type-builds, --source-type-release-bundles, --source-type-application-versions, --source-type-packages, --source-type-artifacts.", }, { name: "empty flags", @@ -166,7 +176,7 @@ func TestCreateAppVersionCommand_FlagsSuite(t *testing.T) { }, expectsPayload: nil, expectsError: true, - errorContains: "At least one source flag is required to create an application version. Please provide --spec or at least one of the following: --source-type-builds, --source-type-release-bundles, --source-type-application-versions.", + errorContains: "At least one source flag is required to create an application version. Please provide --spec or at least one of the following: --source-type-builds, --source-type-release-bundles, --source-type-application-versions, --source-type-packages, --source-type-artifacts.", }, } @@ -405,6 +415,134 @@ func TestParseSourceVersions(t *testing.T) { } } +func TestParsePackages(t *testing.T) { + cmd := &createAppVersionCommand{} + + tests := []struct { + name string + input string + expectError bool + errorContains string + expectedPackages []model.CreateVersionPackage + }{ + { + name: "multiple packages", + input: "type=npm,name=pkg1,version=1.0.0,repo-key=repo1;type=docker,name=pkg2,version=2.0.0,repo-key=repo2", + expectError: false, + expectedPackages: []model.CreateVersionPackage{ + {Type: "npm", Name: "pkg1", Version: "1.0.0", Repository: "repo1"}, + {Type: "docker", Name: "pkg2", Version: "2.0.0", Repository: "repo2"}, + }, + }, + { + name: "empty string", + input: "", + expectError: false, + expectedPackages: nil, + }, + { + name: "missing type field", + input: "name=pkg1,version=1.0.0,repo-key=repo1", + expectError: true, + errorContains: "missing required field: type", + }, + { + name: "missing name field", + input: "type=npm,version=1.0.0,repo-key=repo1", + expectError: true, + errorContains: "missing required field: name", + }, + { + name: "missing version field", + input: "type=npm,name=pkg1,repo-key=repo1", + expectError: true, + errorContains: "missing required field: version", + }, + { + name: "missing repo-key field", + input: "type=npm,name=pkg1,version=1.0.0", + expectError: true, + errorContains: "missing required field: repo-key", + }, + { + name: "invalid format", + input: "pkg1", + expectError: true, + errorContains: "invalid package format", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + packages, err := cmd.parsePackages(tt.input) + if tt.expectError { + assert.Error(t, err) + if tt.errorContains != "" { + assert.Contains(t, err.Error(), tt.errorContains) + } + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expectedPackages, packages) + } + }) + } +} + +func TestParseArtifacts(t *testing.T) { + cmd := &createAppVersionCommand{} + + tests := []struct { + name string + input string + expectError bool + errorContains string + expectedArtifacts []model.CreateVersionArtifact + }{ + { + name: "multiple artifacts", + input: "path=repo/path/to/artifact1.jar,sha256=abc123def456;path=repo/path/to/artifact2.war", + expectError: false, + expectedArtifacts: []model.CreateVersionArtifact{ + {Path: "repo/path/to/artifact1.jar", SHA256: "abc123def456"}, + {Path: "repo/path/to/artifact2.war"}, + }, + }, + { + name: "empty string", + input: "", + expectError: false, + expectedArtifacts: nil, + }, + { + name: "missing path field", + input: "sha256=abc123def456", + expectError: true, + errorContains: "missing required field: path", + }, + { + name: "invalid format", + input: "artifact1.jar", + expectError: true, + errorContains: "invalid artifact format", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + artifacts, err := cmd.parseArtifacts(tt.input) + if tt.expectError { + assert.Error(t, err) + if tt.errorContains != "" { + assert.Contains(t, err.Error(), tt.errorContains) + } + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expectedArtifacts, artifacts) + } + }) + } +} + func TestCreateAppVersionCommand_SpecFileSuite(t *testing.T) { tests := []struct { name string @@ -462,6 +600,95 @@ func TestCreateAppVersionCommand_SpecFileSuite(t *testing.T) { expectsError: true, errorContains: "Spec file is empty", }, + { + name: "artifacts spec file", + specPath: "./testfiles/artifacts-spec.json", + args: []string{"app-artifacts", "1.0.0"}, + expectsPayload: &model.CreateAppVersionRequest{ + ApplicationKey: "app-artifacts", + Version: "1.0.0", + Sources: &model.CreateVersionSources{ + Artifacts: []model.CreateVersionArtifact{ + { + Path: "repo/path/to/artifact1.jar", + SHA256: "abc123def456", + }, + { + Path: "repo/path/to/artifact2.war", + }, + }, + }, + }, + }, + { + name: "all sources spec file", + specPath: "./testfiles/all-sources-spec.json", + args: []string{"app-all-sources", "5.0.0"}, + expectsPayload: &model.CreateAppVersionRequest{ + ApplicationKey: "app-all-sources", + Version: "5.0.0", + Sources: &model.CreateVersionSources{ + Artifacts: []model.CreateVersionArtifact{ + { + Path: "repo/path/to/app.jar", + SHA256: "abc123def456789", + }, + { + Path: "repo/path/to/lib.war", + }, + }, + Packages: []model.CreateVersionPackage{ + { + Type: "npm", + Name: "my-package", + Version: "1.2.3", + Repository: "npm-local", + }, + { + Type: "docker", + Name: "my-docker-image", + Version: "2.0.0", + Repository: "docker-local", + }, + }, + Builds: []model.CreateVersionBuild{ + { + Name: "my-build", + Number: "123", + IncludeDependencies: true, + }, + { + Name: "another-build", + Number: "456", + RepositoryKey: "build-info", + IncludeDependencies: false, + }, + }, + ReleaseBundles: []model.CreateVersionReleaseBundle{ + { + Name: "my-release-bundle", + Version: "1.0.0", + ProjectKey: "my-project", + RepositoryKey: "rb-repo", + }, + { + Name: "another-bundle", + Version: "2.0.0", + }, + }, + Versions: []model.CreateVersionReference{ + { + ApplicationKey: "dependency-app-1", + Version: "3.0.0", + }, + { + ApplicationKey: "dependency-app-2", + Version: "4.5.6", + }, + }, + }, + }, + }, } for _, tt := range tests { @@ -527,6 +754,22 @@ func TestValidateCreateAppVersionContext(t *testing.T) { }, expectError: false, }, + { + name: "valid context with packages flag", + ctxSetup: func(ctx *components.Context) { + ctx.Arguments = []string{"app-key", "1.0.0"} + ctx.AddStringFlag(commands.SourceTypePackagesFlag, "type=npm,name=pkg1,version=1.0.0,repo-key=repo1") + }, + expectError: false, + }, + { + name: "valid context with artifacts flag", + ctxSetup: func(ctx *components.Context) { + ctx.Arguments = []string{"app-key", "1.0.0"} + ctx.AddStringFlag(commands.SourceTypeArtifactsFlag, "path=repo/path/to/artifact1.jar") + }, + expectError: false, + }, { name: "valid context with spec flag", ctxSetup: func(ctx *components.Context) { @@ -589,6 +832,24 @@ func TestValidateNoSpecAndFlagsTogether(t *testing.T) { expectError: true, errorContains: "--spec provided", }, + { + name: "spec flag with packages flag", + ctxSetup: func(ctx *components.Context) { + ctx.AddStringFlag(commands.SpecFlag, "./testfiles/minimal-spec.json") + ctx.AddStringFlag(commands.SourceTypePackagesFlag, "type=npm,name=pkg1,version=1.0.0,repo-key=repo1") + }, + expectError: true, + errorContains: "--spec provided", + }, + { + name: "spec flag with artifacts flag", + ctxSetup: func(ctx *components.Context) { + ctx.AddStringFlag(commands.SpecFlag, "./testfiles/minimal-spec.json") + ctx.AddStringFlag(commands.SourceTypeArtifactsFlag, "path=repo/path/to/artifact1.jar") + }, + expectError: true, + errorContains: "--spec provided", + }, { name: "spec flag only", ctxSetup: func(ctx *components.Context) { diff --git a/apptrust/commands/version/testfiles/all-sources-spec.json b/apptrust/commands/version/testfiles/all-sources-spec.json new file mode 100644 index 0000000..47b7b26 --- /dev/null +++ b/apptrust/commands/version/testfiles/all-sources-spec.json @@ -0,0 +1,60 @@ +{ + "artifacts": [ + { + "path": "repo/path/to/app.jar", + "sha256": "abc123def456789" + }, + { + "path": "repo/path/to/lib.war" + } + ], + "packages": [ + { + "type": "npm", + "name": "my-package", + "version": "1.2.3", + "repository_key": "npm-local" + }, + { + "type": "docker", + "name": "my-docker-image", + "version": "2.0.0", + "repository_key": "docker-local" + } + ], + "builds": [ + { + "name": "my-build", + "number": "123", + "include_dependencies": true + }, + { + "name": "another-build", + "number": "456", + "repository_key": "build-info", + "include_dependencies": false + } + ], + "release_bundles": [ + { + "name": "my-release-bundle", + "version": "1.0.0", + "project_key": "my-project", + "repository_key": "rb-repo" + }, + { + "name": "another-bundle", + "version": "2.0.0" + } + ], + "versions": [ + { + "application_key": "dependency-app-1", + "version": "3.0.0" + }, + { + "application_key": "dependency-app-2", + "version": "4.5.6" + } + ] +} diff --git a/apptrust/commands/version/testfiles/artifacts-spec.json b/apptrust/commands/version/testfiles/artifacts-spec.json new file mode 100644 index 0000000..940525e --- /dev/null +++ b/apptrust/commands/version/testfiles/artifacts-spec.json @@ -0,0 +1,11 @@ +{ + "artifacts": [ + { + "path": "repo/path/to/artifact1.jar", + "sha256": "abc123def456" + }, + { + "path": "repo/path/to/artifact2.war" + } + ] +} diff --git a/apptrust/model/create_app_version_request.go b/apptrust/model/create_app_version_request.go index 6ba7b9d..3ea3a7f 100644 --- a/apptrust/model/create_app_version_request.go +++ b/apptrust/model/create_app_version_request.go @@ -15,6 +15,7 @@ type CreateVersionPackage struct { } type CreateVersionSources struct { + Artifacts []CreateVersionArtifact `json:"artifacts,omitempty"` Packages []CreateVersionPackage `json:"packages,omitempty"` Builds []CreateVersionBuild `json:"builds,omitempty"` ReleaseBundles []CreateVersionReleaseBundle `json:"release_bundles,omitempty"`