Skip to content
This repository has been archived by the owner on Sep 28, 2021. It is now read-only.

Commit

Permalink
App import (#309)
Browse files Browse the repository at this point in the history
* Add base url method, fixed meta-url bug
* Added import command
  • Loading branch information
glooms committed Jun 10, 2019
1 parent 801852f commit c12ca1d
Show file tree
Hide file tree
Showing 10 changed files with 148 additions and 6 deletions.
1 change: 0 additions & 1 deletion .gitignore
Expand Up @@ -2,7 +2,6 @@
vendor/
.vscode/
.DS_Store
*.qvf
*.exe
corectl
corectl.exe
Expand Down
21 changes: 19 additions & 2 deletions cmd/app.go
Expand Up @@ -9,7 +9,7 @@ import (
"github.com/spf13/viper"
)

var getAppsCmd = &cobra.Command{
var listAppsCmd = &cobra.Command{
Use: "ls",
Args: cobra.ExactArgs(0),
Short: "Print a list of all apps available in the current engine",
Expand Down Expand Up @@ -47,6 +47,23 @@ var removeAppCmd = withLocalFlags(&cobra.Command{
},
}, "suppress")

var importAppCmd = &cobra.Command{
Use: "import",
Args: cobra.ExactArgs(1),
Short: "Import the specified app into the engine, returns the ID of the created app",
Long: "Import the specified app into the engine, returns the ID of the created app",
Example: "corectl import <path-to-app.qvf>",
Annotations: map[string]string{
"x-qlik-stability": "experimental",
},

Run: func(ccmd *cobra.Command, args []string) {
appPath := args[0]
appID := internal.ImportApp(appPath, viper.GetString("engine"), headers)
fmt.Println("Imported app with new ID: " + appID)
},
}

var appCmd = &cobra.Command{
Use: "app",
Short: "Explore and manage apps",
Expand All @@ -57,5 +74,5 @@ var appCmd = &cobra.Command{
}

func init() {
appCmd.AddCommand(getAppsCmd, removeAppCmd)
appCmd.AddCommand(listAppsCmd, removeAppCmd, importAppCmd)
}
1 change: 1 addition & 0 deletions docs/corectl_app.md
Expand Up @@ -30,6 +30,7 @@ Explore and manage apps
### SEE ALSO

* [corectl](corectl.md) -
* [corectl app import](corectl_app_import.md) - Import the specified app into the engine, returns the ID of the created app
* [corectl app ls](corectl_app_ls.md) - Print a list of all apps available in the current engine
* [corectl app rm](corectl_app_rm.md) - Remove the specified app

43 changes: 43 additions & 0 deletions docs/corectl_app_import.md
@@ -0,0 +1,43 @@
## corectl app import

Import the specified app into the engine, returns the ID of the created app

### Synopsis

Import the specified app into the engine, returns the ID of the created app

```
corectl app import [flags]
```

### Examples

```
corectl import <path-to-app.qvf>
```

### Options

```
-h, --help help for import
```

### Options inherited from parent commands

```
-a, --app string Name or identifier of the app
--certificates string path/to/folder containing client.pem, client_key.pem and root.pem certificates
-c, --config string path/to/config.yml where parameters can be set instead of on the command line
-e, --engine string URL to the Qlik Associative Engine (default "localhost:9076")
--headers stringToString Http headers to use when connecting to Qlik Associative Engine (default [])
--json Returns output in JSON format if possible, disables verbose and traffic output
--no-data Open app without data
-t, --traffic Log JSON websocket traffic to stdout
--ttl string Qlik Associative Engine session time to live in seconds (default "0")
-v, --verbose Log extra information
```

### SEE ALSO

* [corectl app](corectl_app.md) - Explore and manage apps

4 changes: 4 additions & 0 deletions docs/spec.json
Expand Up @@ -60,6 +60,10 @@
"app": {
"description": "Explore and manage apps",
"commands": {
"import": {
"description": "Import the specified app into the engine, returns the ID of the created app",
"x-qlik-stability": "experimental"
},
"ls": {
"description": "Print a list of all apps available in the current engine"
},
Expand Down
41 changes: 41 additions & 0 deletions internal/restapi.go
Expand Up @@ -5,6 +5,8 @@ import (
"fmt"
"io/ioutil"
"net/http"
neturl "net/url"
"os"
)

// ReadRestMetadata fetches metadata from the rest api.
Expand Down Expand Up @@ -33,6 +35,41 @@ func ReadRestMetadata(url string, headers http.Header) (*RestMetadata, error) {
return result, nil
}

func ImportApp(appPath, engine string, headers http.Header) string {
url, err := neturl.Parse(buildRestBaseURL(engine))
url.Path = "/v1/apps/import"
// You can specify name and mode with query but they only seem to work in ABAC mode.
values := neturl.Values{}
url.RawQuery = values.Encode()
file, err := os.Open(appPath)
if err != nil {
FatalError("could not open file: ", appPath)
}
defer file.Close()
req, err := http.NewRequest("POST", url.String(), file)
if err != nil {
FatalError(err)
}
req.Header = headers
req.Header.Add("Content-Type", "binary/octet-stream")
response, err := http.DefaultClient.Do(req)
if err != nil {
FatalError(err)
}
defer response.Body.Close()
data, _ := ioutil.ReadAll(response.Body)
if response.StatusCode != 200 {
FatalErrorf("could not import app: got status %d with message %s",
response.StatusCode, string(data))
}
appInfo := &RestNxApp{}
json.Unmarshal(data, appInfo)
appID := appInfo.Attributes["id"]
appName := appInfo.Attributes["name"]
setAppIDToKnownApps(engine, appName, appID, false)
return appID
}

func toFieldMetadataMap(fields []*RestFieldMetadata) map[string]*RestFieldMetadata {
result := make(map[string]*RestFieldMetadata)
for _, field := range fields {
Expand Down Expand Up @@ -131,3 +168,7 @@ type RestFieldMetadata struct {
// Static RAM memory used in bytes.
ByteSize int `json:"byte_size"`
}

type RestNxApp struct {
Attributes map[string]string `json:"attributes"`
}
12 changes: 9 additions & 3 deletions internal/state.go
Expand Up @@ -258,10 +258,16 @@ func buildWebSocketURL(engine string, ttl string) string {
return engine
}

func buildRestBaseURL(engine string) string {
engineURL := TidyUpEngineURL(engine)
u, _ := neturl.Parse(engineURL)
// u.Scheme[2:] is "" for ws and s for wss
baseURL := "http" + u.Scheme[2:] + "://" + u.Host
return baseURL
}

func buildMetadataURL(engine string, appID string) string {
engine = TidyUpEngineURL(engine)
engine = strings.Replace(engine, "wss://", "https://", -1)
engine = strings.Replace(engine, "ws://", "http://", -1)
engine = buildRestBaseURL(engine)
url := fmt.Sprintf("%s/v1/apps/%s/data/metadata", engine, neturl.QueryEscape(appID))
return url
}
Expand Down
11 changes: 11 additions & 0 deletions internal/state_test.go
Expand Up @@ -6,13 +6,24 @@ import (
"github.com/magiconair/properties/assert"
)

func TestBuildRestBaseUrl(t *testing.T) {
assert.Equal(t, buildRestBaseURL("engine"), "http://engine")
assert.Equal(t, buildRestBaseURL("engine.com"), "http://engine.com")
assert.Equal(t, buildRestBaseURL("engine.com/app/appId"), "http://engine.com")
assert.Equal(t, buildRestBaseURL("engine:1234"), "http://engine:1234")
assert.Equal(t, buildRestBaseURL("ws://engine:1234"), "http://engine:1234")
assert.Equal(t, buildRestBaseURL("wss://engine:1234"), "https://engine:1234")
assert.Equal(t, buildRestBaseURL("wss://engine:1234/app/appId"), "https://engine:1234")
}

func TestBuildMetaUrl(t *testing.T) {
assert.Equal(t, buildMetadataURL("engine", "appId"), "http://engine/v1/apps/appId/data/metadata")
assert.Equal(t, buildMetadataURL("engine:1234", "appId"), "http://engine:1234/v1/apps/appId/data/metadata")
assert.Equal(t, buildMetadataURL("wss://engine", "appId"), "https://engine/v1/apps/appId/data/metadata")
assert.Equal(t, buildMetadataURL("ws://engine", "appId"), "http://engine/v1/apps/appId/data/metadata")
assert.Equal(t, buildMetadataURL("wss://engine:1234", "appId"), "https://engine:1234/v1/apps/appId/data/metadata")
assert.Equal(t, buildMetadataURL("ws://engine:1234", "appId"), "http://engine:1234/v1/apps/appId/data/metadata")
assert.Equal(t, buildMetadataURL("ws://engine:1234/app/appId", "appId"), "http://engine:1234/v1/apps/appId/data/metadata")
}

func TestBuildEngineUrl(t *testing.T) {
Expand Down
20 changes: 20 additions & 0 deletions test/corectl_integration_test.go
Expand Up @@ -6,6 +6,7 @@ import (
"github.com/qlik-oss/corectl/test/toolkit"
"os"
"testing"
"strings"
)

func TestBasicAnalyzing(t *testing.T) {
Expand Down Expand Up @@ -392,5 +393,24 @@ func TestCommandLineOverridingConfigFile(t *testing.T) {
p2.ExpectJsonArray("qId", "swedish-dimension").Run("dimension", "ls", "--json")
p2.ExpectJsonArray("qId", "measure-x").Run("measure", "ls", "--json")
p2.ExpectJsonArray("qName", "bogusname").Run("connection", "ls", "--json")
}

func TestImportApp(t *testing.T) {
parseID := func (output []byte) string {
appID := strings.Split(string(output), ": ")[1]
return strings.TrimSpace(appID)
}
// Create tests for the standard, abac and jwt case.
pStd := toolkit.Params{T: t, Engine: *toolkit.EngineStdIP}
pAbac := toolkit.Params{T: t, Engine: *toolkit.EngineAbacIP}
pJwt := toolkit.Params{T: t, Engine: *toolkit.EngineJwtIP, Config: "test/projects/using-jwts/corectl.yml"}
params := []toolkit.Params{pStd, pAbac, pJwt}
for _, p := range params {
// See if we can import the app test.qvf
output := p.ExpectOK().Run("app", "import", "test/projects/import/test.qvf")
appID := parseID(output)
// If it was created, we can remove it
p.ExpectOK().Run("app", "rm", appID, "--suppress")
p.Reset()
}
}
Binary file added test/projects/import/test.qvf
Binary file not shown.

0 comments on commit c12ca1d

Please sign in to comment.