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
138 changes: 138 additions & 0 deletions apptrust/commands/application/export_app_cmd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package application

import (
"fmt"
"os"
"path/filepath"

"github.com/jfrog/jfrog-cli-application/apptrust/app"
commonCLiCommands "github.com/jfrog/jfrog-cli-core/v2/common/commands"
pluginsCommon "github.com/jfrog/jfrog-cli-core/v2/plugins/common"

"github.com/jfrog/jfrog-cli-application/apptrust/commands"
"github.com/jfrog/jfrog-cli-application/apptrust/commands/utils"
"github.com/jfrog/jfrog-cli-application/apptrust/common"
"github.com/jfrog/jfrog-cli-application/apptrust/service"
"github.com/jfrog/jfrog-cli-application/apptrust/service/applications"
"github.com/jfrog/jfrog-cli-core/v2/plugins/components"
coreConfig "github.com/jfrog/jfrog-cli-core/v2/utils/config"
"github.com/jfrog/jfrog-client-go/utils/errorutils"
"github.com/jfrog/jfrog-client-go/utils/io/fileutils"
"github.com/jfrog/jfrog-client-go/utils/log"
)

type exportAppCommand struct {
serverDetails *coreConfig.ServerDetails
applicationService applications.ApplicationService
applicationKey string
targetPath string
}

func (eac *exportAppCommand) Run() error {
ctx, err := service.NewContext(*eac.serverDetails)
if err != nil {
return err
}

applicationEnvelope, err := eac.applicationService.ExportApplication(ctx, eac.applicationKey)
if err != nil {
return err
}

if err := os.MkdirAll(filepath.Dir(eac.targetPath), 0o755); err != nil {
return errorutils.CheckError(err)
}
if err := os.WriteFile(eac.targetPath, applicationEnvelope, 0o644); err != nil {
return errorutils.CheckError(err)
}

log.Info(fmt.Sprintf("Application export written to %s", eac.targetPath))
return nil
}

func (eac *exportAppCommand) ServerDetails() (*coreConfig.ServerDetails, error) {
return eac.serverDetails, nil
}

func (eac *exportAppCommand) CommandName() string {
return commands.AppExport
}

func (eac *exportAppCommand) prepareAndRunCommand(ctx *components.Context) error {
if len(ctx.Arguments) < 1 || len(ctx.Arguments) > 2 {
return pluginsCommon.WrongNumberOfArgumentsHandler(ctx)
}

eac.applicationKey = ctx.Arguments[0]
target := ""
if len(ctx.Arguments) == 2 {
target = ctx.Arguments[1]
}
eac.targetPath = resolveExportTargetPath(eac.applicationKey, target)

var err error
eac.serverDetails, err = utils.ServerDetailsByFlags(ctx)
if err != nil {
return err
}

return commonCLiCommands.Exec(eac)
}

func resolveExportTargetPath(applicationKey, target string) string {
if target == "" {
target = "./"
}

// If the target ends with a slash, treat it as a directory and append the default filename
dir, fileName := fileutils.GetLocalPathAndFile(applicationKey+".json", "", target, true, false)
return filepath.Join(dir, fileName)
}

func GetExportAppCommand(appContext app.Context) components.Command {
cmd := &exportAppCommand{
applicationService: appContext.GetApplicationService(),
}
return components.Command{
Name: commands.AppExport,
Description: "Export an application to a local JSON file.",
AIDescription: `Export an application's AppTrust metadata (descriptor, owners, labels, monitor policy, package bindings) to a local JSON file for air-gap transfer.

When to use:
- Copy application metadata between disconnected AppTrust instances.

Prerequisites:
- The application must exist.
- Configured server and project-admin permission on the application's project.
- Export is not available on Edge nodes.

Common patterns:
$ jf apptrust app-export my-app
$ jf apptrust app-export my-app ./exports/
$ jf apptrust app-export my-app ./my-app.json
$ jf at aexp my-app ./exports/ --server-id=my-server

Gotchas:
- This exports application metadata only, not versions or artifacts.
- Trailing slash on target = directory ({application-key}.json is written inside); no slash = rename to that file.
- An existing file at the target path is overwritten without a prompt.

Related: jf apptrust app-import, jf apptrust app-create, jf apptrust app-delete`,
Category: common.CategoryApplication,
Aliases: []string{"aexp"},
Arguments: []components.Argument{
{
Name: "application-key",
Description: "The key of the application to export.",
Optional: false,
},
{
Name: "target pattern",
Description: "Local filesystem target path. If it ends with a slash, it is assumed to be a directory and {application-key}.json is written into it. If there is no terminal slash, the target path is assumed to be a file to which the export file should be renamed.",
Optional: true,
},
},
Flags: commands.GetCommandFlags(commands.AppExport),
Action: cmd.prepareAndRunCommand,
}
}
104 changes: 104 additions & 0 deletions apptrust/commands/application/export_app_cmd_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package application

import (
"errors"
"flag"
"os"
"path/filepath"
"testing"

mockapps "github.com/jfrog/jfrog-cli-application/apptrust/service/applications/mocks"
"github.com/jfrog/jfrog-cli-core/v2/plugins/components"
"github.com/jfrog/jfrog-cli-core/v2/utils/config"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/urfave/cli"
"go.uber.org/mock/gomock"
)

func TestExportAppCommand_Run(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()

serverDetails := &config.ServerDetails{Url: "https://example.com"}
appKey := "app-key"
targetPath := filepath.Join(t.TempDir(), "app.json")
compactJSON := []byte(`{"applicationKey":"app-key","schemaVersion":1}`)

mockAppService := mockapps.NewMockApplicationService(ctrl)
mockAppService.EXPECT().ExportApplication(gomock.Any(), appKey).Return(compactJSON, nil).Times(1)

cmd := &exportAppCommand{
applicationService: mockAppService,
serverDetails: serverDetails,
applicationKey: appKey,
targetPath: targetPath,
}

err := cmd.Run()
assert.NoError(t, err)

got, err := os.ReadFile(targetPath)
require.NoError(t, err)
assert.Equal(t, compactJSON, got)
}

func TestExportAppCommand_Run_Error(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()

serverDetails := &config.ServerDetails{Url: "https://example.com"}
appKey := "app-key"

mockAppService := mockapps.NewMockApplicationService(ctrl)
mockAppService.EXPECT().ExportApplication(gomock.Any(), appKey).Return(nil, errors.New("export error")).Times(1)

cmd := &exportAppCommand{
applicationService: mockAppService,
serverDetails: serverDetails,
applicationKey: appKey,
targetPath: filepath.Join(t.TempDir(), "app.json"),
}

err := cmd.Run()
assert.Error(t, err)
assert.Equal(t, "export error", err.Error())
}

func TestExportAppCommand_WrongNumberOfArguments(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
app := cli.NewApp()
set := flag.NewFlagSet("test", 0)
ctx := cli.NewContext(app, set, nil)

mockAppService := mockapps.NewMockApplicationService(ctrl)
cmd := &exportAppCommand{
applicationService: mockAppService,
}

// Test with no arguments
context, err := components.ConvertContext(ctx)
assert.NoError(t, err)

err = cmd.prepareAndRunCommand(context)
assert.Error(t, err)
assert.Contains(t, err.Error(), "Wrong number of arguments")
}

func TestResolveExportTargetPath(t *testing.T) {
tests := []struct {
name string
target string
want string
}{
{name: "omitted defaults to current directory", target: "", want: "my-app.json"},
{name: "trailing slash is a directory", target: "exports/", want: filepath.Join("exports", "my-app.json")},
{name: "no trailing slash is a rename file", target: filepath.Join("a", "b"), want: filepath.Join("a", "b")},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, resolveExportTargetPath("my-app", tt.target))
})
}
}
98 changes: 98 additions & 0 deletions apptrust/commands/application/import_app_cmd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package application

import (
"github.com/jfrog/jfrog-cli-application/apptrust/app"
commonCLiCommands "github.com/jfrog/jfrog-cli-core/v2/common/commands"
pluginsCommon "github.com/jfrog/jfrog-cli-core/v2/plugins/common"

"github.com/jfrog/jfrog-cli-application/apptrust/commands"
"github.com/jfrog/jfrog-cli-application/apptrust/commands/utils"
"github.com/jfrog/jfrog-cli-application/apptrust/common"
"github.com/jfrog/jfrog-cli-application/apptrust/service"
"github.com/jfrog/jfrog-cli-application/apptrust/service/applications"
"github.com/jfrog/jfrog-cli-core/v2/plugins/components"
coreConfig "github.com/jfrog/jfrog-cli-core/v2/utils/config"
"github.com/jfrog/jfrog-client-go/utils/errorutils"
"github.com/jfrog/jfrog-client-go/utils/io/fileutils"
)

type importAppCommand struct {
serverDetails *coreConfig.ServerDetails
applicationService applications.ApplicationService
applicationEnvelope []byte
}

func (iac *importAppCommand) Run() error {
ctx, err := service.NewContext(*iac.serverDetails)
if err != nil {
return err
}

return iac.applicationService.ImportApplication(ctx, iac.applicationEnvelope)
}

func (iac *importAppCommand) ServerDetails() (*coreConfig.ServerDetails, error) {
return iac.serverDetails, nil
}

func (iac *importAppCommand) CommandName() string {
return commands.AppImport
}

func (iac *importAppCommand) prepareAndRunCommand(ctx *components.Context) error {
if len(ctx.Arguments) != 1 {
return pluginsCommon.WrongNumberOfArgumentsHandler(ctx)
}

content, err := fileutils.ReadFile(ctx.Arguments[0])
if errorutils.CheckError(err) != nil {
return err
}
iac.applicationEnvelope = content

iac.serverDetails, err = utils.ServerDetailsByFlags(ctx)
if err != nil {
return err
}

return commonCLiCommands.Exec(iac)
}

func GetImportAppCommand(appContext app.Context) components.Command {
cmd := &importAppCommand{
applicationService: appContext.GetApplicationService(),
}
return components.Command{
Name: commands.AppImport,
Description: "Import an application from a local file.",
AIDescription: `Import application metadata (descriptor, owners, labels, monitor policy, package bindings) from a local export file into AppTrust.

When to use:
- Restore or copy application metadata onto an AppTrust instance after jf apptrust app-export.

Prerequisites:
- A file produced by app-export.
- Configured server and project-admin permission on the project's key in the file.

Common patterns:
$ jf apptrust app-import ./my-app
$ jf at aimp ./my-app.json --server-id=my-server

Gotchas:
- This imports application metadata only, not versions or artifacts.
- Import upserts the application in place; a mismatched projectKey in the file is rejected by the server.

Related: jf apptrust app-export, jf apptrust app-create, jf apptrust app-delete`,
Category: common.CategoryApplication,
Aliases: []string{"aimp"},
Arguments: []components.Argument{
{
Name: "path to file",
Description: "Local path to the application export file.",
Optional: false,
},
},
Flags: commands.GetCommandFlags(commands.AppImport),
Action: cmd.prepareAndRunCommand,
}
}
Loading
Loading