blob: b37b059e61bdf4442f400a2c259a354e0ffc875e [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package utils
import (
"archive/zip"
"bufio"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"os/user"
"path/filepath"
"reflect"
"strings"
"crypto/tls"
"github.com/apache/incubator-openwhisk-client-go/whisk"
"github.com/apache/incubator-openwhisk-wskdeploy/wski18n"
"github.com/hokaccha/go-prettyjson"
"io/ioutil"
"net/http"
"path"
"time"
)
const (
DEFAULT_HTTP_TIMEOUT = 30
DEFAULT_PROJECT_PATH = "."
// name of manifest and deployment files
ManifestFileNameYaml = "manifest.yaml"
ManifestFileNameYml = "manifest.yml"
DeploymentFileNameYaml = "deployment.yaml"
DeploymentFileNameYml = "deployment.yml"
)
// ActionRecord is a container to keep track of
// a whisk action struct and a location filepath we use to
// map files and manifest declared actions
type ActionRecord struct {
Action *whisk.Action
Packagename string
Filepath string
}
type TriggerRecord struct {
Trigger *whisk.Trigger
Packagename string
}
type RuleRecord struct {
Rule *whisk.Rule
Packagename string
}
func GetHomeDirectory() string {
usr, err := user.Current()
if err != nil {
return ""
}
return usr.HomeDir
}
// Potentially complex structures(such as DeploymentProject, DeploymentPackage)
// could implement those interface which is convenient for put, get subtract in
// containers etc.
type Comparable interface {
HashCode() uint32
Equals() bool
}
func IsFeedAction(trigger *whisk.Trigger) (string, bool) {
for _, annotation := range trigger.Annotations {
if annotation.Key == "feed" {
return annotation.Value.(string), true
}
}
return "", false
}
func PrettyJSON(j interface{}) (string, error) {
formatter := prettyjson.NewFormatter()
bytes, err := formatter.Marshal(j)
if err != nil {
return "", err
}
return string(bytes), nil
}
// Common utilities
// Prompt for user input
func Ask(reader *bufio.Reader, question string, def string) string {
fmt.Print(question + " (" + def + "): ")
answer, _ := reader.ReadString('\n')
len := len(answer)
if len == 1 {
return def
}
return answer[:len-1]
}
// Test if a string
func isValidEnvironmentVar(value string) bool {
// A valid Env. variable should start with or contain '$' (dollar) char.
//
// If the value is a single Env. variable, it should start with a '$' (dollar) char
// and have at least 1 additional character after it, e.g. $ENV_VAR
// If the value is a concatenation of a string and a Env. variable, it should contain '$' (dollar)
// and have a string following which is surrounded with '{' and '}', e.g. xxx${ENV_VAR}xxx.
if value != "" && strings.HasPrefix(value, "$") && len(value) > 1 {
return true
}
if value != "" && strings.Contains(value,"${") && strings.Count(value,"{")==strings.Count(value,"}") {
return true
}
return false
}
// Get the env variable value by key.
// Get the env variable if the key is start by $
func GetEnvVar(key interface{}) interface{} {
// Assure the key itself is not nil
if key == nil {
return nil
}
if reflect.TypeOf(key).String() == "string" {
keystr := key.(string)
if isValidEnvironmentVar(keystr) {
// retrieve the value of the env. var. from the host system.
var thisValue string
//split the string with ${}
//test if the substr is a environment var
//if it is, replace it with the value
f := func(c rune) bool {
return c=='$' || c=='{' || c=='}'
}
for _,substr := range strings.FieldsFunc(keystr, f) {
//if the substr is a $ENV_VAR
if strings.Contains(keystr,"$"+substr) {
thisValue = os.Getenv(substr)
if thisValue == "" {
PrintOpenWhiskOutputln("WARNING: Missing Environment Variable " + substr + ".")
}
keystr = strings.Replace(keystr,"$"+substr,thisValue,-1)
//if the substr is a ${ENV_VAR}
} else if strings.Contains(keystr,"${"+substr+"}") {
thisValue = os.Getenv(substr)
if thisValue == "" {
PrintOpenWhiskOutputln("WARNING: Missing Environment Variable " + substr + ".")
}
keystr = strings.Replace(keystr,"${"+substr+"}",thisValue,-1)
}
}
return keystr
}
// The key was not a valid env. variable, simply return it as the value itself (of type string)
return keystr
}
return key
}
var kindToJSON []string = []string{"", "boolean", "integer", "integer", "integer", "integer", "integer", "integer", "integer", "integer",
"integer", "integer", "integer", "number", "number", "number", "number", "array", "", "", "", "object", "", "", "string", "", ""}
// Gets JSON type name
func GetJSONType(j interface{}) string {
fmt.Print(reflect.TypeOf(j).Kind())
return kindToJSON[reflect.TypeOf(j).Kind()]
}
func NewZipWritter(src, des string) *ZipWritter {
zw := &ZipWritter{src: src, des: des}
return zw
}
type ZipWritter struct {
src string
des string
zipWritter *zip.Writer
}
func (zw *ZipWritter) zipFile(path string, f os.FileInfo, err error) error {
if err != nil {
return err
}
if !f.Mode().IsRegular() || f.Size() == 0 {
return nil
}
file, err := os.Open(path)
if err != nil {
return err
}
defer file.Close()
fileName := strings.TrimPrefix(path, zw.src+"/")
wr, err := zw.zipWritter.Create(fileName)
if err != nil {
return err
}
_, err = io.Copy(wr, file)
if err != nil {
return err
}
return nil
}
func (zw *ZipWritter) Zip() error {
// create zip file
zipFile, err := os.Create(zw.des)
if err != nil {
return err
}
defer zipFile.Close()
zw.zipWritter = zip.NewWriter(zipFile)
err = filepath.Walk(zw.src, zw.zipFile)
if err != nil {
return nil
}
err = zw.zipWritter.Close()
if err != nil {
return err
}
return nil
}
// below codes is from wsk cli with tiny adjusts.
func GetExec(artifact string, kind string, isDocker bool, mainEntry string) (*whisk.Exec, error) {
var err error
var code string
var content []byte
var exec *whisk.Exec
ext := filepath.Ext(artifact)
exec = new(whisk.Exec)
if !isDocker || ext == ".zip" {
content, err = new(ContentReader).ReadLocal(artifact)
if err != nil {
return nil, err
}
code = string(content)
exec.Code = &code
}
if len(kind) > 0 {
exec.Kind = kind
} else if isDocker {
exec.Kind = "blackbox"
if ext != ".zip" {
exec.Image = artifact
} else {
exec.Image = "openwhisk/dockerskeleton"
}
} else if ext == ".swift" {
exec.Kind = "swift:default"
} else if ext == ".js" {
exec.Kind = "nodejs:default"
} else if ext == ".py" {
exec.Kind = "python:default"
} else if ext == ".jar" {
exec.Kind = "java:default"
exec.Code = nil
} else {
if ext == ".zip" {
return nil, zipKindError()
} else {
return nil, extensionError(ext)
}
}
// Error if entry point is not specified for Java
if len(mainEntry) != 0 {
exec.Main = mainEntry
} else {
if exec.Kind == "java" {
return nil, javaEntryError()
}
}
// Base64 encode the zip file content
if ext == ".zip" {
code = base64.StdEncoding.EncodeToString([]byte(code))
exec.Code = &code
}
return exec, nil
}
func zipKindError() error {
errMsg := wski18n.T("creating an action from a .zip artifact requires specifying the action kind explicitly")
return errors.New(errMsg)
}
func extensionError(extension string) error {
errMsg := wski18n.T(
"'{{.name}}' is not a supported action runtime",
map[string]interface{}{
"name": extension,
})
return errors.New(errMsg)
}
func javaEntryError() error {
errMsg := wski18n.T("Java actions require --main to specify the fully-qualified name of the main class")
return errors.New(errMsg)
}
//for web action support, code from wsk cli with tiny adjustments
const WEB_EXPORT_ANNOT = "web-export"
const RAW_HTTP_ANNOT = "raw-http"
const FINAL_ANNOT = "final"
func WebAction(webMode string, annotations whisk.KeyValueArr, entityName string, fetch bool) (whisk.KeyValueArr, error) {
switch strings.ToLower(webMode) {
case "yes":
fallthrough
case "true":
return webActionAnnotations(fetch, annotations, entityName, addWebAnnotations)
case "no":
fallthrough
case "false":
return webActionAnnotations(fetch, annotations, entityName, deleteWebAnnotations)
case "raw":
return webActionAnnotations(fetch, annotations, entityName, addRawAnnotations)
default:
return nil, errors.New(webMode)
}
}
type WebActionAnnotationMethod func(annotations whisk.KeyValueArr) whisk.KeyValueArr
func webActionAnnotations(
fetchAnnotations bool,
annotations whisk.KeyValueArr,
entityName string,
webActionAnnotationMethod WebActionAnnotationMethod) (whisk.KeyValueArr, error) {
if annotations != nil || !fetchAnnotations {
annotations = webActionAnnotationMethod(annotations)
}
return annotations, nil
}
func addWebAnnotations(annotations whisk.KeyValueArr) whisk.KeyValueArr {
annotations = deleteWebAnnotationKeys(annotations)
annotations = addKeyValue(WEB_EXPORT_ANNOT, true, annotations)
annotations = addKeyValue(RAW_HTTP_ANNOT, false, annotations)
annotations = addKeyValue(FINAL_ANNOT, true, annotations)
return annotations
}
func deleteWebAnnotations(annotations whisk.KeyValueArr) whisk.KeyValueArr {
annotations = deleteWebAnnotationKeys(annotations)
annotations = addKeyValue(WEB_EXPORT_ANNOT, false, annotations)
annotations = addKeyValue(RAW_HTTP_ANNOT, false, annotations)
annotations = addKeyValue(FINAL_ANNOT, false, annotations)
return annotations
}
func addRawAnnotations(annotations whisk.KeyValueArr) whisk.KeyValueArr {
annotations = deleteWebAnnotationKeys(annotations)
annotations = addKeyValue(WEB_EXPORT_ANNOT, true, annotations)
annotations = addKeyValue(RAW_HTTP_ANNOT, true, annotations)
annotations = addKeyValue(FINAL_ANNOT, true, annotations)
return annotations
}
func deleteWebAnnotationKeys(annotations whisk.KeyValueArr) whisk.KeyValueArr {
annotations = deleteKey(WEB_EXPORT_ANNOT, annotations)
annotations = deleteKey(RAW_HTTP_ANNOT, annotations)
annotations = deleteKey(FINAL_ANNOT, annotations)
return annotations
}
func deleteKey(key string, keyValueArr whisk.KeyValueArr) whisk.KeyValueArr {
for i := 0; i < len(keyValueArr); i++ {
if keyValueArr[i].Key == key {
keyValueArr = append(keyValueArr[:i], keyValueArr[i+1:]...)
break
}
}
return keyValueArr
}
func addKeyValue(key string, value interface{}, keyValueArr whisk.KeyValueArr) whisk.KeyValueArr {
keyValue := whisk.KeyValue{
Key: key,
Value: value,
}
return append(keyValueArr, keyValue)
}
// Structs used to denote the OpenWhisk Runtime information
type Limit struct {
Apm uint16 `json:"actions_per_minute"`
Tpm uint16 `json:"triggers_per_minute"`
ConAction uint16 `json:"concurrent_actions"`
}
type Runtime struct {
Image string `json:"image"`
Deprecated bool `json:"deprecated"`
ReMain bool `json:"requireMain"`
Default bool `json:"default"`
Attach bool `json:"attached"`
Kind string `json:"kind"`
}
type SupportInfo struct {
Github string `json:"github"`
Slack string `json:"slack"`
}
type OpenWhiskInfo struct {
Support SupportInfo `json:"support"`
Desc string `json:"description"`
ApiPath []string `json:"api_paths"`
Runtimes map[string][]Runtime `json:"runtimes"`
Limits Limit `json:"limits"`
}
// We could get the openwhisk info from bluemix through running the command
// `curl -k https://openwhisk.ng.bluemix.net`
// hard coding it here in case of network unavailable or failure.
func ParseOpenWhisk(apiHost string) (op OpenWhiskInfo, err error) {
ct := "application/json; charset=UTF-8"
req, _ := http.NewRequest("GET", "https://"+apiHost, nil)
req.Header.Set("Content-Type", ct)
tlsConfig := &tls.Config{
InsecureSkipVerify: true,
}
var netTransport = &http.Transport{
TLSClientConfig: tlsConfig,
}
var netClient = &http.Client{
Timeout: time.Second * DEFAULT_HTTP_TIMEOUT,
Transport: netTransport,
}
res, err := netClient.Do(req)
if err != nil {
errString := wski18n.T("Failed to get the supported runtimes from OpenWhisk service: {{.err}}.\n",
map[string]interface{}{"err": err.Error()})
whisk.Debug(whisk.DbgWarn, errString)
}
if res != nil {
defer res.Body.Close()
}
// Local openwhisk deployment sometimes only returns "application/json" as the content type
if err != nil || !strings.Contains(ct, res.Header.Get("Content-Type")) {
stdout := wski18n.T("Start to unmarshal Openwhisk info from local values.\n")
whisk.Debug(whisk.DbgInfo, stdout)
err = json.Unmarshal(runtimeInfo, &op)
} else {
b, _ := ioutil.ReadAll(res.Body)
if b != nil && len(b) > 0 {
stdout := wski18n.T("Unmarshal Openwhisk info from internet.\n")
whisk.Debug(whisk.DbgInfo, stdout)
err = json.Unmarshal(b, &op)
}
}
return
}
func ConvertToMap(op OpenWhiskInfo) (rt map[string][]string) {
rt = make(map[string][]string)
for k, v := range op.Runtimes {
rt[k] = make([]string, 0, len(v))
for i := range v {
if (!v[i].Deprecated) {
rt[k] = append(rt[k], v[i].Kind)
}
}
}
return
}
var runtimeInfo = []byte(`{
"support": {
"github": "https://github.com/apache/incubator-openwhisk/issues",
"slack": "http://slack.openwhisk.org"
},
"description": "OpenWhisk",
"api_paths": ["/api/v1"],
"runtimes": {
"nodejs": [{
"image": "openwhisk/nodejsaction:latest",
"deprecated": true,
"requireMain": false,
"default": false,
"attached": false,
"kind": "nodejs"
}, {
"image": "openwhisk/nodejs6action:latest",
"deprecated": false,
"requireMain": false,
"default": true,
"attached": false,
"kind": "nodejs:6"
}],
"java": [{
"image": "openwhisk/java8action:latest",
"deprecated": false,
"requireMain": true,
"default": true,
"attached": true,
"kind": "java"
}],
"php": [{
"image": "openwhisk/action-php-v7.1:latest",
"deprecated": false,
"requireMain": false,
"default": true,
"attached": false,
"kind": "php:7.1"
}],
"python": [{
"image": "openwhisk/python2action:latest",
"deprecated": false,
"requireMain": false,
"default": false,
"attached": false,
"kind": "python"
}, {
"image": "openwhisk/python2action:latest",
"deprecated": false,
"requireMain": false,
"default": true,
"attached": false,
"kind": "python:2"
}, {
"image": "openwhisk/python3action:latest",
"deprecated": false,
"requireMain": false,
"default": false,
"attached": false,
"kind": "python:3"
}],
"swift": [{
"image": "openwhisk/swiftaction:latest",
"deprecated": true,
"requireMain": false,
"default": false,
"attached": false,
"kind": "swift"
}, {
"image": "openwhisk/swift3action:latest",
"deprecated": false,
"requireMain": false,
"default": true,
"attached": false,
"kind": "swift:3"
}, {
"image": "openwhisk/action-swift-v3.1.1:latest",
"deprecated": false,
"requireMain": false,
"default": false,
"attached": false,
"kind": "swift:3.1.1"
}]
},
"limits": {
"actions_per_minute": 5000,
"triggers_per_minute": 5000,
"concurrent_actions": 1000
}
}
`)
var Rts map[string][]string
var DefaultRts = map[string][]string{
"nodejs": {"nodejs", "nodejs:6"},
"java": {"java"},
"php": {"php:7.1"},
"python": {"python", "python:2", "python:3"},
"swift": {"swift", "swift:3", "swift:3.1.1"},
}
func CheckExistRuntime(rtname string, rts map[string][]string) bool {
for _, v := range rts {
for i := range v {
if rtname == v[i] {
return true
}
}
}
return false
}
func GetManifestFilePath(projectPath string) string {
if _, err := os.Stat(path.Join(projectPath, ManifestFileNameYaml)); err == nil {
return path.Join(projectPath, ManifestFileNameYaml)
} else if _, err := os.Stat(path.Join(projectPath, ManifestFileNameYml)); err == nil {
return path.Join(projectPath, ManifestFileNameYml)
} else {
return ""
}
}
func GetDeploymentFilePath(projectPath string) string {
if _, err := os.Stat(path.Join(projectPath, DeploymentFileNameYaml)); err == nil {
return path.Join(projectPath, DeploymentFileNameYaml)
} else if _, err := os.Stat(path.Join(projectPath, DeploymentFileNameYml)); err == nil {
return path.Join(projectPath, DeploymentFileNameYml)
} else {
return ""
}
}
// agnostic util reader to fetch content from web or local path or potentially other places.
type ContentReader struct {
URLReader
LocalReader
}
type URLReader struct {
}
func (urlReader *URLReader) ReadUrl(url string) (content []byte, err error) {
resp, err := http.Get(url)
if err != nil {
return content, err
}
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return content, err
} else {
defer resp.Body.Close()
}
return b, nil
}
type LocalReader struct {
}
func (localReader *LocalReader) ReadLocal(path string) ([]byte, error) {
cont, err := ioutil.ReadFile(path)
return cont, err
}
func Read(url string) ([]byte, error) {
if strings.HasPrefix(url, "http") {
return new(ContentReader).URLReader.ReadUrl(url)
} else {
return new(ContentReader).LocalReader.ReadLocal(url)
}
}