diff --git a/application/app/auth/app_details.go b/application/app/auth/app_details.go new file mode 100644 index 0000000..faa47b8 --- /dev/null +++ b/application/app/auth/app_details.go @@ -0,0 +1,15 @@ +package auth + +import "github.com/jfrog/jfrog-client-go/auth" + +type appDetails struct { + auth.CommonConfigFields +} + +func NewAppDetails() auth.ServiceDetails { + return &appDetails{} +} + +func (rt *appDetails) GetVersion() (string, error) { + panic("Failed: Method is not implemented") +} diff --git a/application/app/context.go b/application/app/context.go new file mode 100644 index 0000000..7109c5a --- /dev/null +++ b/application/app/context.go @@ -0,0 +1,35 @@ +package app + +import ( + "github.com/jfrog/jfrog-cli-application/application/service" +) + +type Context interface { + GetVersionService() service.VersionService + GetSystemService() service.SystemService + GetConfig() interface{} +} + +type context struct { + versionService service.VersionService + systemService service.SystemService +} + +func NewAppContext() Context { + return &context{ + versionService: service.NewVersionService(), + systemService: service.NewSystemService(), + } +} + +func (c *context) GetVersionService() service.VersionService { + return c.versionService +} + +func (c *context) GetSystemService() service.SystemService { + return c.systemService +} + +func (c *context) GetConfig() interface{} { + return nil +} diff --git a/application/cli/cli.go b/application/cli/cli.go index 43c25c2..8cff548 100644 --- a/application/cli/cli.go +++ b/application/cli/cli.go @@ -1,21 +1,43 @@ package cli import ( + "github.com/jfrog/jfrog-cli-application/application/app" + "github.com/jfrog/jfrog-cli-application/application/commands/system" + "github.com/jfrog/jfrog-cli-application/application/commands/version" "github.com/jfrog/jfrog-cli-core/v2/plugins/components" ) const category = "Application Lifecycle" +// +//func GetJfrogApplicationCli() components.App { +// appContext := app.NewAppContext() +// appEntity := components.CreateEmbeddedApp( +// category, +// nil, +// components.Namespace{ +// Name: "app", +// Description: "Tools for Application Lifecycle management", +// Category: category, +// Commands: []components.Command{ +// system.GetPingCommand(appContext), +// version.GetCreateAppVersionCommand(appContext), +// }, +// }, +// ) +// return appEntity +//} + func GetJfrogApplicationCli() components.App { - app := components.CreateEmbeddedApp( - category, - nil, - components.Namespace{ - Name: "app", - Description: "Tools for Application Lifecycle management", - Category: category, - Commands: []components.Command{}, + appContext := app.NewAppContext() + appEntity := components.CreateApp( + "app", + "1.0.0", + "JFrog Application CLI", + []components.Command{ + system.GetPingCommand(appContext), + version.GetCreateAppVersionCommand(appContext), }, ) - return app + return appEntity } diff --git a/application/commands/flags.go b/application/commands/flags.go new file mode 100644 index 0000000..9b31bc7 --- /dev/null +++ b/application/commands/flags.go @@ -0,0 +1,71 @@ +package commands + +import pluginsCommon "github.com/jfrog/jfrog-cli-core/v2/plugins/common" +import "github.com/jfrog/jfrog-cli-core/v2/plugins/components" + +const ( + Ping = "ping" + CreateAppVersion = "create-app-version" +) + +const ( + ServerId = "server-id" + url = "url" + user = "user" + accessToken = "access-token" + ProjectFlag = "project" + + ApplicationKeyFlag = "app-key" + PackageTypeFlag = "package-type" + PackageNameFlag = "package-name" + PackageVersionFlag = "package-version" + PackageRepositoryFlag = "package-repository" + SpecFlag = "spec" + SpecVarsFlag = "spec-vars" +) + +// Flag keys mapped to their corresponding components.Flag definition. +var flagsMap = map[string]components.Flag{ + // Common commands flags + ServerId: components.NewStringFlag(ServerId, "Server ID configured using the config command.", func(f *components.StringFlag) { f.Mandatory = false }), + url: components.NewStringFlag(url, "JFrog Platform URL.", func(f *components.StringFlag) { f.Mandatory = false }), + user: components.NewStringFlag(user, "JFrog username.", func(f *components.StringFlag) { f.Mandatory = false }), + accessToken: components.NewStringFlag(accessToken, "JFrog access token.", func(f *components.StringFlag) { f.Mandatory = false }), + ProjectFlag: components.NewStringFlag(ProjectFlag, "Project key associated with the created evidence.", func(f *components.StringFlag) { f.Mandatory = false }), + + ApplicationKeyFlag: components.NewStringFlag(ApplicationKeyFlag, "Application key.", func(f *components.StringFlag) { f.Mandatory = true }), + PackageTypeFlag: components.NewStringFlag(PackageTypeFlag, "Package type.", func(f *components.StringFlag) { f.Mandatory = false }), + PackageNameFlag: components.NewStringFlag(PackageNameFlag, "Package name.", func(f *components.StringFlag) { f.Mandatory = false }), + PackageVersionFlag: components.NewStringFlag(PackageVersionFlag, "Package version.", func(f *components.StringFlag) { f.Mandatory = false }), + PackageRepositoryFlag: components.NewStringFlag(PackageRepositoryFlag, "Package storing repository.", func(f *components.StringFlag) { f.Mandatory = false }), + SpecFlag: components.NewStringFlag(SpecFlag, "A path to the specification file.", func(f *components.StringFlag) { f.Mandatory = false }), + SpecVarsFlag: components.NewStringFlag(SpecVarsFlag, "List of semicolon-separated(;) variables in the form of \"key1=value1;key2=value2;...\" (wrapped by quotes) to be replaced in the File Spec. In the File Spec, the variables should be used as follows: ${key1}.` `", func(f *components.StringFlag) { f.Mandatory = false }), +} + +var commandFlags = map[string][]string{ + CreateAppVersion: { + url, + user, + accessToken, + ServerId, + ProjectFlag, + ApplicationKeyFlag, + PackageTypeFlag, + PackageNameFlag, + PackageVersionFlag, + PackageRepositoryFlag, + SpecFlag, + SpecVarsFlag, + }, + + Ping: { + url, + user, + accessToken, + ServerId, + }, +} + +func GetCommandFlags(cmdKey string) []components.Flag { + return pluginsCommon.GetCommandFlags(cmdKey, commandFlags, flagsMap) +} diff --git a/application/commands/system/ping_cmd.go b/application/commands/system/ping_cmd.go new file mode 100644 index 0000000..8b3e7c8 --- /dev/null +++ b/application/commands/system/ping_cmd.go @@ -0,0 +1,52 @@ +package system + +import ( + "github.com/jfrog/jfrog-cli-application/application/app" + "github.com/jfrog/jfrog-cli-application/application/commands" + "github.com/jfrog/jfrog-cli-application/application/commands/utils" + "github.com/jfrog/jfrog-cli-application/application/common" + "github.com/jfrog/jfrog-cli-application/application/service" + commonCLiCommands "github.com/jfrog/jfrog-cli-core/v2/common/commands" + "github.com/jfrog/jfrog-cli-core/v2/plugins/components" + coreConfig "github.com/jfrog/jfrog-cli-core/v2/utils/config" +) + +type pingCommand struct { + systemService service.SystemService + serverDetails *coreConfig.ServerDetails +} + +func (pc *pingCommand) Run() error { + ctx := &service.Context{ServerDetails: pc.serverDetails} + return pc.systemService.Ping(ctx) +} + +func (pc *pingCommand) ServerDetails() (*coreConfig.ServerDetails, error) { + return pc.serverDetails, nil +} + +func (pc *pingCommand) CommandName() string { + return commands.Ping +} + +func (pc *pingCommand) prepareAndRunCommand(ctx *components.Context) error { + serverDetails, err := utils.ServerDetailsByFlags(ctx) + if err != nil { + return err + } + pc.serverDetails = serverDetails + return commonCLiCommands.Exec(pc) +} + +func GetPingCommand(appContext app.Context) components.Command { + cmd := &pingCommand{systemService: appContext.GetSystemService()} + return components.Command{ + Name: commands.Ping, + Description: "Ping the application server", + Category: common.CategorySystem, + Aliases: []string{"p"}, + Arguments: []components.Argument{}, + Flags: commands.GetCommandFlags(commands.Ping), + Action: cmd.prepareAndRunCommand, + } +} diff --git a/application/commands/utils/utils.go b/application/commands/utils/utils.go new file mode 100644 index 0000000..19dc57a --- /dev/null +++ b/application/commands/utils/utils.go @@ -0,0 +1,39 @@ +package utils + +import ( + "fmt" + commonCliUtils "github.com/jfrog/jfrog-cli-core/v2/common/cliutils" + pluginsCommon "github.com/jfrog/jfrog-cli-core/v2/plugins/common" + "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" + "github.com/jfrog/jfrog-client-go/utils/errorutils" +) + +func AssertValueProvided(c *components.Context, fieldName string) error { + if c.GetStringFlagValue(fieldName) == "" { + return errorutils.CheckErrorf("the --%s option is mandatory", fieldName) + } + return nil +} + +func PlatformToApplicationUrls(details *coreConfig.ServerDetails) { + details.ArtifactoryUrl = utils.AddTrailingSlashIfNeeded(details.Url) + "artifactory/" + details.EvidenceUrl = utils.AddTrailingSlashIfNeeded(details.Url) + "evidence/" + details.MetadataUrl = utils.AddTrailingSlashIfNeeded(details.Url) + "metadata/" +} + +func ServerDetailsByFlags(ctx *components.Context) (*coreConfig.ServerDetails, error) { + serverDetails, err := pluginsCommon.CreateServerDetailsWithConfigOffer(ctx, true, commonCliUtils.Platform) + if err != nil { + return nil, err + } + if serverDetails.Url == "" { + return nil, fmt.Errorf("platform URL is mandatory for evidence commands") + } + if serverDetails.GetUser() != "" && serverDetails.GetPassword() != "" { + return nil, fmt.Errorf("evidence service does not support basic authentication") + } + + return serverDetails, nil +} diff --git a/application/commands/version/create_app_version_cmd.go b/application/commands/version/create_app_version_cmd.go new file mode 100644 index 0000000..15ed167 --- /dev/null +++ b/application/commands/version/create_app_version_cmd.go @@ -0,0 +1,157 @@ +package version + +import ( + "encoding/json" + "github.com/jfrog/jfrog-cli-application/application/app" + "github.com/jfrog/jfrog-cli-application/application/commands" + "github.com/jfrog/jfrog-cli-application/application/commands/utils" + "github.com/jfrog/jfrog-cli-application/application/common" + "github.com/jfrog/jfrog-cli-application/application/model" + "github.com/jfrog/jfrog-cli-application/application/service" + 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-core/v2/plugins/components" + coreConfig "github.com/jfrog/jfrog-cli-core/v2/utils/config" + "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" + "github.com/jfrog/jfrog-client-go/utils/errorutils" + "github.com/jfrog/jfrog-client-go/utils/io/fileutils" +) + +type createAppVersionCommand struct { + versionService service.VersionService + serverDetails *coreConfig.ServerDetails + requestPayload *model.CreateAppVersionRequest +} + +type createVersionSpec struct { + Packages []model.CreateVersionPackage `json:"packages"` +} + +func (cv *createAppVersionCommand) Run() error { + ctx := &service.Context{ServerDetails: cv.serverDetails} + return cv.versionService.CreateAppVersion(ctx, cv.requestPayload) +} + +func (cv *createAppVersionCommand) ServerDetails() (*coreConfig.ServerDetails, error) { + return cv.serverDetails, nil +} + +func (cv *createAppVersionCommand) CommandName() string { + return commands.CreateAppVersion +} + +func (cv *createAppVersionCommand) prepareAndRunCommand(ctx *components.Context) error { + if err := validateCreateAppVersionContext(ctx); err != nil { + return err + } + serverDetails, err := utils.ServerDetailsByFlags(ctx) + if err != nil { + return err + } + cv.serverDetails = serverDetails + cv.requestPayload, err = cv.buildRequestPayload(ctx) + if errorutils.CheckError(err) != nil { + return err + } + return commonCLiCommands.Exec(cv) +} + +func (cv *createAppVersionCommand) buildRequestPayload(ctx *components.Context) (*model.CreateAppVersionRequest, error) { + var packages []model.CreateVersionPackage + if ctx.IsFlagSet(commands.SpecFlag) { + err := loadPackagesFromSpec(ctx, &packages) + if errorutils.CheckError(err) != nil { + return nil, err + } + } else { + packages = append(packages, model.CreateVersionPackage{ + Type: ctx.GetStringFlagValue(commands.PackageTypeFlag), + Name: ctx.GetStringFlagValue(commands.PackageNameFlag), + Version: ctx.GetStringFlagValue(commands.PackageVersionFlag), + Repository: ctx.GetStringFlagValue(commands.PackageRepositoryFlag), + }) + } + + return &model.CreateAppVersionRequest{ + ApplicationKey: ctx.GetStringFlagValue(commands.ApplicationKeyFlag), + Version: ctx.Arguments[0], + Packages: packages, + }, nil +} + +func loadPackagesFromSpec(ctx *components.Context, packages *[]model.CreateVersionPackage) error { + specFilePath := ctx.GetStringFlagValue(commands.SpecFlag) + spec := new(createVersionSpec) + specVars := coreutils.SpecVarsStringToMap(ctx.GetStringFlagValue("spec-vars")) + content, err := fileutils.ReadFile(specFilePath) + if errorutils.CheckError(err) != nil { + return err + } + + if len(specVars) > 0 { + content = coreutils.ReplaceVars(content, specVars) + } + + err = json.Unmarshal(content, spec) + if errorutils.CheckError(err) != nil { + return err + } + + // add spec packages to the packages list + for _, pkg := range spec.Packages { + *packages = append(*packages, pkg) + } + return nil +} + +func validateCreateAppVersionContext(ctx *components.Context) error { + if show, err := pluginsCommon.ShowCmdHelpIfNeeded(ctx, ctx.Arguments); show || err != nil { + return err + } + if len(ctx.Arguments) != 1 { + return pluginsCommon.WrongNumberOfArgumentsHandler(ctx) + } + + // Use spec flag if provided, if not check for package flags + err := utils.AssertValueProvided(ctx, commands.SpecFlag) + if err != nil { + err = utils.AssertValueProvided(ctx, commands.PackageNameFlag) + if err != nil { + return handleMissingPackageDetailsError() + } + err = utils.AssertValueProvided(ctx, commands.PackageVersionFlag) + if err != nil { + return handleMissingPackageDetailsError() + } + err = utils.AssertValueProvided(ctx, commands.PackageRepositoryFlag) + if err != nil { + return handleMissingPackageDetailsError() + } + } + + return nil +} + +func GetCreateAppVersionCommand(appContext app.Context) components.Command { + cmd := &createAppVersionCommand{versionService: appContext.GetVersionService()} + return components.Command{ + Name: commands.CreateAppVersion, + Description: "Create application version", + Category: common.CategoryVersion, + Aliases: []string{"cav"}, + Arguments: []components.Argument{ + { + Name: "version-name", + Description: "The name of the version", + Optional: false, + }, + }, + Flags: commands.GetCommandFlags(commands.CreateAppVersion), + Action: cmd.prepareAndRunCommand, + } +} + +func handleMissingPackageDetailsError() error { + return errorutils.CheckErrorf("Missing packages information. Please provide the following flags --%s or the set of: --%s, --%s, --%s, --%s", + commands.SpecFlag, commands.PackageTypeFlag, commands.PackageNameFlag, commands.PackageVersionFlag, commands.PackageRepositoryFlag) +} diff --git a/application/common/categories.go b/application/common/categories.go new file mode 100644 index 0000000..c06cb4f --- /dev/null +++ b/application/common/categories.go @@ -0,0 +1,6 @@ +package common + +const ( + CategorySystem = "system" + CategoryVersion = "version" +) diff --git a/application/http/http_client.go b/application/http/http_client.go new file mode 100644 index 0000000..baf64be --- /dev/null +++ b/application/http/http_client.go @@ -0,0 +1,124 @@ +package http + +import ( + "encoding/json" + "fmt" + commonCliConfig "github.com/jfrog/jfrog-cli-core/v2/utils/config" + "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" + "github.com/jfrog/jfrog-client-go/auth" + clientConfig "github.com/jfrog/jfrog-client-go/config" + "github.com/jfrog/jfrog-client-go/http/jfroghttpclient" + "github.com/jfrog/jfrog-client-go/utils" + "github.com/jfrog/jfrog-client-go/utils/errorutils" + "github.com/jfrog/jfrog-client-go/utils/io/httputils" + "net/http" +) + +const applicationApiPath = "application/api" + +type AppHttpClient interface { + GetHttpClient() *jfroghttpclient.JfrogHttpClient + Post(path string, requestBody interface{}) (resp *http.Response, body []byte, err error) + Get(path string) (resp *http.Response, body []byte, err error) +} + +type appHttpClient struct { + client *jfroghttpclient.JfrogHttpClient + serverDetails *commonCliConfig.ServerDetails + authDetails auth.ServiceDetails + serviceConfig clientConfig.Config +} + +func NewAppHttpClient(serverDetails *commonCliConfig.ServerDetails) (AppHttpClient, error) { + certsPath, err := coreutils.GetJfrogCertsDir() + if err != nil { + return nil, err + } + + authDetails, err := serverDetails.CreateLifecycleAuthConfig() + if err != nil { + return nil, err + } + + serviceConfig, err := clientConfig.NewConfigBuilder(). + SetServiceDetails(authDetails). + SetCertificatesPath(certsPath). + SetInsecureTls(serverDetails.InsecureTls). + Build() + if err != nil { + return nil, err + } + + jfHttpClient, err := jfroghttpclient.JfrogClientBuilder(). + SetCertificatesPath(certsPath). + SetInsecureTls(serviceConfig.IsInsecureTls()). + SetClientCertPath(serverDetails.GetClientCertPath()). + SetClientCertKeyPath(serverDetails.GetClientCertKeyPath()). + AppendPreRequestInterceptor(authDetails.RunPreRequestFunctions). + SetContext(serviceConfig.GetContext()). + SetDialTimeout(serviceConfig.GetDialTimeout()). + SetOverallRequestTimeout(serviceConfig.GetOverallRequestTimeout()). + SetRetries(serviceConfig.GetHttpRetries()). + SetRetryWaitMilliSecs(serviceConfig.GetHttpRetryWaitMilliSecs()). + Build() + + if err != nil { + return nil, err + } + + appClient := &appHttpClient{ + client: jfHttpClient, + serverDetails: serverDetails, + authDetails: authDetails, + serviceConfig: serviceConfig, + } + return appClient, nil +} + +func (c *appHttpClient) GetHttpClient() *jfroghttpclient.JfrogHttpClient { + return c.client +} + +func (c *appHttpClient) Post(path string, requestBody interface{}) (resp *http.Response, body []byte, err error) { + url, err := utils.BuildUrl(c.serverDetails.Url, applicationApiPath+path, nil) + if err != nil { + return nil, nil, err + } + + requestContent, err := c.toJsonBytes(requestBody) + if err != nil { + return nil, nil, err + } + + println("url: ", url) + return c.client.SendPost(url, requestContent, c.getJsonHttpClientDetails()) +} + +func (c *appHttpClient) Get(path string) (resp *http.Response, body []byte, err error) { + url, err := utils.BuildUrl(c.serverDetails.Url, applicationApiPath+path, nil) + if err != nil { + return nil, nil, err + } + + response, body, _, err := c.client.SendGet(url, false, c.getJsonHttpClientDetails()) + return response, body, err +} + +func (c *appHttpClient) toJsonBytes(payload interface{}) ([]byte, error) { + if payload == nil { + return nil, fmt.Errorf("request payload is required") + } + + jsonBytes, err := json.Marshal(payload) + if err != nil { + return nil, errorutils.CheckError(err) + } + return jsonBytes, nil +} + +func (c *appHttpClient) getJsonHttpClientDetails() *httputils.HttpClientDetails { + var httpClientDetails httputils.HttpClientDetails + httpClientDetails = c.authDetails.CreateHttpClientDetails() + httpClientDetails.SetContentTypeApplicationJson() + return &httpClientDetails +} diff --git a/application/model/create_app_version_request.go b/application/model/create_app_version_request.go new file mode 100644 index 0000000..13eaf4b --- /dev/null +++ b/application/model/create_app_version_request.go @@ -0,0 +1,14 @@ +package model + +type CreateAppVersionRequest struct { + ApplicationKey string `json:"application_key"` + Version string `json:"version"` + Packages []CreateVersionPackage `json:"packages"` +} + +type CreateVersionPackage struct { + Type string `json:"type"` + Name string `json:"name"` + Version string `json:"version"` + Repository string `json:"repository"` +} diff --git a/application/service/context.go b/application/service/context.go new file mode 100644 index 0000000..8b59970 --- /dev/null +++ b/application/service/context.go @@ -0,0 +1,7 @@ +package service + +import coreConfig "github.com/jfrog/jfrog-cli-core/v2/utils/config" + +type Context struct { + ServerDetails *coreConfig.ServerDetails +} diff --git a/application/service/system_service.go b/application/service/system_service.go new file mode 100644 index 0000000..3efe396 --- /dev/null +++ b/application/service/system_service.go @@ -0,0 +1,36 @@ +package service + +import ( + "fmt" + "github.com/jfrog/jfrog-cli-application/application/http" +) + +type SystemService interface { + Ping(ctx *Context) error +} + +type systemService struct { +} + +func NewSystemService() SystemService { + return &systemService{} +} + +func (ss *systemService) Ping(ctx *Context) error { + httpClient, err := http.NewAppHttpClient(ctx.ServerDetails) + if err != nil { + return err + } + + response, body, err := httpClient.Get("/v1/system/ping") + if err != nil { + return err + } + + if response.StatusCode != 200 { + return fmt.Errorf("failed to create app version. Status code: %d", response.StatusCode) + } + + fmt.Println(string(body)) + return nil +} diff --git a/application/service/version_service.go b/application/service/version_service.go new file mode 100644 index 0000000..7393ce1 --- /dev/null +++ b/application/service/version_service.go @@ -0,0 +1,37 @@ +package service + +import ( + "fmt" + "github.com/jfrog/jfrog-cli-application/application/http" + "github.com/jfrog/jfrog-cli-application/application/model" +) + +type VersionService interface { + CreateAppVersion(ctx *Context, request *model.CreateAppVersionRequest) error +} + +type versionService struct { +} + +func NewVersionService() VersionService { + return &versionService{} +} + +func (vs *versionService) CreateAppVersion(ctx *Context, request *model.CreateAppVersionRequest) error { + httpClient, err := http.NewAppHttpClient(ctx.ServerDetails) + if err != nil { + return err + } + + response, responseBody, err := httpClient.Post("/v1/version", request) + if err != nil { + return err + } + + if response.StatusCode != 201 { + return fmt.Errorf("failed to create app version. Status code: %d. \n%s", + response.StatusCode, responseBody) + } + + return nil +}