| /* |
| * 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 parsers |
| |
| import ( |
| "encoding/base64" |
| "encoding/json" |
| "errors" |
| "io/ioutil" |
| "os" |
| "path" |
| "path/filepath" |
| "strings" |
| |
| "gopkg.in/yaml.v2" |
| |
| "net/url" |
| |
| "github.com/apache/openwhisk-client-go/whisk" |
| "github.com/apache/openwhisk-wskdeploy/conductor" |
| "github.com/apache/openwhisk-wskdeploy/dependencies" |
| "github.com/apache/openwhisk-wskdeploy/runtimes" |
| "github.com/apache/openwhisk-wskdeploy/utils" |
| "github.com/apache/openwhisk-wskdeploy/webaction" |
| "github.com/apache/openwhisk-wskdeploy/wskderrors" |
| "github.com/apache/openwhisk-wskdeploy/wskenv" |
| "github.com/apache/openwhisk-wskdeploy/wski18n" |
| "github.com/apache/openwhisk-wskdeploy/wskprint" |
| yamlHelper "github.com/ghodss/yaml" |
| ) |
| |
| const ( |
| API = "API" |
| HTTPS = "https://" |
| HTTP = "http://" |
| API_VERSION = "v1" |
| WEB = "web" |
| PATH_SEPARATOR = "/" |
| DEFAULT_PACKAGE = "default" |
| NATIVE_DOCKER_IMAGE = "openwhisk/dockerskeleton" |
| PARAM_OPENING_BRACKET = "{" |
| PARAM_CLOSING_BRACKET = "}" |
| |
| DUMMY_APIGW_ACCESS_TOKEN = "DUMMY TOKEN" |
| |
| YAML_FILE_EXTENSION = "yaml" |
| YML_FILE_EXTENSION = "yml" |
| ) |
| |
| // Read existing manifest file or create new if none exists |
| func ReadOrCreateManifest() (*YAML, error) { |
| maniyaml := YAML{} |
| |
| if _, err := os.Stat(utils.ManifestFileNameYaml); err == nil { |
| dat, _ := ioutil.ReadFile(utils.ManifestFileNameYaml) |
| err := NewYAMLParser().Unmarshal(dat, &maniyaml) |
| if err != nil { |
| return &maniyaml, wskderrors.NewFileReadError(utils.ManifestFileNameYaml, err.Error()) |
| } |
| } |
| return &maniyaml, nil |
| } |
| |
| // Serialize manifest to local file |
| func Write(manifest *YAML, filename string) error { |
| output, err := NewYAMLParser().marshal(manifest) |
| if err != nil { |
| return wskderrors.NewYAMLFileFormatError(filename, err.Error()) |
| } |
| |
| f, err := os.Create(filename) |
| if err != nil { |
| return wskderrors.NewFileReadError(filename, err.Error()) |
| } |
| defer f.Close() |
| |
| f.Write(output) |
| return nil |
| } |
| |
| func (dm *YAMLParser) Unmarshal(input []byte, manifest *YAML) error { |
| err := yaml.UnmarshalStrict(input, manifest) |
| if err != nil { |
| return err |
| } |
| return nil |
| } |
| |
| func (dm *YAMLParser) marshal(manifest *YAML) (output []byte, err error) { |
| data, err := yaml.Marshal(manifest) |
| if err != nil { |
| return nil, err |
| } |
| return data, nil |
| } |
| |
| func (dm *YAMLParser) ParseManifest(manifestPath string) (*YAML, error) { |
| mm := NewYAMLParser() |
| maniyaml := YAML{} |
| |
| content, err := utils.Read(manifestPath) |
| if err != nil { |
| return &maniyaml, wskderrors.NewFileReadError(manifestPath, err.Error()) |
| } |
| |
| err = mm.Unmarshal(content, &maniyaml) |
| if err != nil { |
| return &maniyaml, wskderrors.NewYAMLParserErr(manifestPath, err) |
| } |
| maniyaml.Filepath = manifestPath |
| manifest := ReadEnvVariable(&maniyaml) |
| |
| return manifest, nil |
| } |
| |
| func (dm *YAMLParser) composeInputs(inputs map[string]Parameter, packageInputs PackageInputs, manifestFilePath string) (whisk.KeyValueArr, error) { |
| var errorParser error |
| keyValArr := make(whisk.KeyValueArr, 0) |
| var inputsWithoutValue []string |
| var paramsCLI interface{} |
| |
| if len(utils.Flags.Param) > 0 { |
| paramsCLI, errorParser = utils.GetJSONFromStrings(utils.Flags.Param, false) |
| if errorParser != nil { |
| return nil, errorParser |
| } |
| } |
| |
| for name, param := range inputs { |
| var keyVal whisk.KeyValue |
| // keyvalue key is set to parameter name |
| keyVal.Key = name |
| // parameter on CLI takes the highest precedence such that |
| // input variables gets values from CLI first |
| if paramsCLI != nil { |
| // check if this particular input is specified on CLI |
| if v, ok := paramsCLI.(map[string]interface{})[name]; ok { |
| keyVal.Value = wskenv.ConvertSingleName(v.(string)) |
| } |
| } |
| // if those inputs are not specified on CLI, |
| // read their values from the manifest file |
| if keyVal.Value == nil { |
| keyVal.Value, errorParser = ResolveParameter(name, ¶m, manifestFilePath) |
| if errorParser != nil { |
| return nil, errorParser |
| } |
| if param.Type == STRING && param.Value != nil { |
| if keyVal.Value == getTypeDefaultValue(param.Type) { |
| if packageInputs.Inputs != nil { |
| n := wskenv.GetEnvVarName(param.Value.(string)) |
| if v, ok := packageInputs.Inputs[n]; ok { |
| keyVal.Value = v.Value.(string) |
| } |
| } |
| } |
| } |
| } |
| if keyVal.Value != nil { |
| keyValArr = append(keyValArr, keyVal) |
| } |
| if param.Required && keyVal.Value == getTypeDefaultValue(param.Type) { |
| inputsWithoutValue = append(inputsWithoutValue, name) |
| } |
| } |
| if len(inputsWithoutValue) > 0 { |
| errMessage := wski18n.T(wski18n.ID_ERR_REQUIRED_INPUTS_MISSING_VALUE_X_inputs_X, |
| map[string]interface{}{ |
| wski18n.KEY_INPUTS: strings.Join(inputsWithoutValue, ", ")}) |
| return nil, wskderrors.NewYAMLFileFormatError(manifestFilePath, errMessage) |
| } |
| |
| return keyValArr, nil |
| } |
| |
| func (dm *YAMLParser) composeAnnotations(annotations map[string]interface{}) whisk.KeyValueArr { |
| listOfAnnotations := make(whisk.KeyValueArr, 0) |
| for name, value := range annotations { |
| var keyVal whisk.KeyValue |
| keyVal.Key = name |
| value = wskenv.InterpolateStringWithEnvVar(value) |
| keyVal.Value = utils.ConvertInterfaceValue(value) |
| listOfAnnotations = append(listOfAnnotations, keyVal) |
| } |
| return listOfAnnotations |
| } |
| |
| func (dm *YAMLParser) ComposeDependenciesFromAllPackages(manifest *YAML, projectPath string, filePath string, managedAnnotations whisk.KeyValue, packageInputs map[string]PackageInputs) (map[string]dependencies.DependencyRecord, error) { |
| dependencies := make(map[string]dependencies.DependencyRecord) |
| packages := make(map[string]Package) |
| |
| if len(manifest.Packages) != 0 { |
| packages = manifest.Packages |
| } else { |
| packages = manifest.GetProject().Packages |
| } |
| |
| for n, p := range packages { |
| d, err := dm.ComposeDependencies(p, projectPath, filePath, n, managedAnnotations, packageInputs[n]) |
| if err == nil { |
| for k, v := range d { |
| dependencies[k] = v |
| } |
| } else { |
| return nil, err |
| } |
| } |
| return dependencies, nil |
| } |
| |
| func (dm *YAMLParser) ComposeDependencies(pkg Package, projectPath string, filePath string, packageName string, managedAnnotations whisk.KeyValue, packageInputs PackageInputs) (map[string]dependencies.DependencyRecord, error) { |
| |
| depMap := make(map[string]dependencies.DependencyRecord) |
| for key, dependency := range pkg.Dependencies { |
| version := dependency.Version |
| if len(version) == 0 { |
| version = YAML_VALUE_BRANCH_MASTER |
| } |
| |
| location := dependency.Location |
| |
| isBinding := false |
| if dependencies.LocationIsBinding(location) { |
| if !strings.HasPrefix(location, PATH_SEPARATOR) { |
| location = PATH_SEPARATOR + dependency.Location |
| } |
| isBinding = true |
| } else if dependencies.LocationIsGithub(location) { |
| |
| // TODO() define const for the protocol prefix, etc. |
| _, err := url.ParseRequestURI(location) |
| if err != nil { |
| location = HTTPS + dependency.Location |
| location = wskenv.InterpolateStringWithEnvVar(location).(string) |
| } |
| isBinding = false |
| } else { |
| // TODO() create new named error in wskerrors package |
| return nil, errors.New(wski18n.T(wski18n.ID_ERR_DEPENDENCY_UNKNOWN_TYPE)) |
| } |
| |
| inputs, err := dm.composeInputs(dependency.Inputs, packageInputs, filePath) |
| if err != nil { |
| return nil, err |
| } |
| |
| annotations := dm.composeAnnotations(dependency.Annotations) |
| |
| if utils.Flags.Managed || utils.Flags.Sync { |
| annotations = append(annotations, managedAnnotations) |
| } |
| |
| packDir := path.Join(projectPath, strings.Title(YAML_KEY_PACKAGES)) |
| depName := packageName + ":" + key |
| depMap[depName] = dependencies.NewDependencyRecord(packDir, packageName, location, version, inputs, annotations, isBinding) |
| } |
| |
| return depMap, nil |
| } |
| |
| func (dm *YAMLParser) ComposeAllPackages(projectInputs map[string]Parameter, manifest *YAML, filePath string, managedAnnotations whisk.KeyValue) (map[string]*whisk.Package, map[string]PackageInputs, error) { |
| packages := map[string]*whisk.Package{} |
| manifestPackages := make(map[string]Package) |
| inputs := make(map[string]PackageInputs, 0) |
| |
| if len(manifest.Packages) != 0 { |
| manifestPackages = manifest.Packages |
| } else { |
| manifestPackages = manifest.GetProject().Packages |
| } |
| |
| if len(manifestPackages) == 0 { |
| warningString := wski18n.T( |
| wski18n.ID_WARN_PACKAGES_NOT_FOUND_X_path_X, |
| map[string]interface{}{ |
| wski18n.KEY_PATH: manifest.Filepath}) |
| wskprint.PrintOpenWhiskWarning(warningString) |
| } |
| |
| // Compose each package found in manifest |
| for n, p := range manifestPackages { |
| s, params, err := dm.ComposePackage(p, n, filePath, managedAnnotations, projectInputs) |
| if err != nil { |
| return nil, inputs, err |
| } |
| packages[n] = s |
| inputs[n] = PackageInputs{PackageName: n, Inputs: params} |
| } |
| |
| return packages, inputs, nil |
| } |
| |
| func (dm *YAMLParser) composePackageInputs(projectInputs map[string]Parameter, rawInputs map[string]Parameter, filepath string) (map[string]Parameter, whisk.KeyValueArr, error) { |
| inputs := make(map[string]Parameter, 0) |
| |
| // package inherits all project inputs |
| for n, param := range projectInputs { |
| inputs[n] = param |
| } |
| |
| // iterate over package inputs |
| for name, i := range rawInputs { |
| value, err := ResolveParameter(name, &i, filepath) |
| if err != nil { |
| return nil, nil, err |
| } |
| // if value is set to default value for its type, |
| // check for input key being an env. variable itself |
| if value == getTypeDefaultValue(i.Type) { |
| value = wskenv.InterpolateStringWithEnvVar("${" + name + "}") |
| } |
| |
| // if at this point, still value is set to default value of its type |
| // check if input key is defined under Project Inputs |
| if value == getTypeDefaultValue(i.Type) { |
| if i.Type == STRING && i.Value != nil { |
| n := wskenv.GetEnvVarName(i.Value.(string)) |
| if v, ok := projectInputs[n]; ok { |
| value = v.Value.(string) |
| } |
| } |
| } |
| |
| // create a Parameter object based on the package inputs |
| // resolve the value using env. variables |
| // if value is not specified, treat input key as an env. variable |
| // check if input key is defined in environment |
| // else set it to its default value based on the type for now |
| // the input value will be updated if its specified in deployment |
| // or on CLI using --param or --param-file |
| i.Value = value |
| inputs[name] = i |
| } |
| |
| // create an array of Key/Value pair with inputs |
| // inputs name as key and with its value if its not nil |
| keyValArr := make(whisk.KeyValueArr, 0) |
| for name, param := range inputs { |
| var keyVal whisk.KeyValue |
| keyVal.Key = name |
| keyVal.Value = param.Value |
| if keyVal.Value != nil { |
| keyValArr = append(keyValArr, keyVal) |
| } |
| } |
| |
| return inputs, keyValArr, nil |
| } |
| |
| func (dm *YAMLParser) ComposePackage(pkg Package, packageName string, filePath string, managedAnnotations whisk.KeyValue, projectInputs map[string]Parameter) (*whisk.Package, map[string]Parameter, error) { |
| pag := &whisk.Package{} |
| pag.Name = packageName |
| //The namespace for this package is absent, so we use default guest here. |
| pag.Namespace = pkg.Namespace |
| pub := false |
| pag.Publish = &pub |
| |
| //Version is a mandatory value |
| //If it is an empty string, it will be set to default value |
| //And print an warning message |
| // TODO(#673) implement STRICT flag |
| if pkg.Version == "" { |
| warningString := wski18n.T( |
| wski18n.ID_WARN_KEY_MISSING_X_key_X_value_X, |
| map[string]interface{}{ |
| wski18n.KEY_KEY: wski18n.PACKAGE_VERSION, |
| wski18n.KEY_VALUE: DEFAULT_PACKAGE_VERSION}) |
| wskprint.PrintOpenWhiskWarning(warningString) |
| |
| warningString = wski18n.T( |
| wski18n.ID_WARN_KEYVALUE_NOT_SAVED_X_key_X, |
| map[string]interface{}{wski18n.KEY_KEY: wski18n.PACKAGE_VERSION}) |
| |
| wskprint.PrintOpenWhiskWarning(warningString) |
| pkg.Version = DEFAULT_PACKAGE_VERSION |
| } |
| pag.Version = wskenv.ConvertSingleName(pkg.Version) |
| |
| //License is a mandatory value |
| //set license to unknown if it is an empty string |
| //And print an warning message |
| // TODO(#673) implement STRICT flag |
| if pkg.License == "" { |
| warningString := wski18n.T( |
| wski18n.ID_WARN_KEY_MISSING_X_key_X_value_X, |
| map[string]interface{}{ |
| wski18n.KEY_KEY: wski18n.PACKAGE_LICENSE, |
| wski18n.KEY_VALUE: DEFAULT_PACKAGE_LICENSE}) |
| wskprint.PrintOpenWhiskWarning(warningString) |
| |
| warningString = wski18n.T( |
| wski18n.ID_WARN_KEYVALUE_NOT_SAVED_X_key_X, |
| map[string]interface{}{wski18n.KEY_KEY: wski18n.PACKAGE_VERSION}) |
| |
| wskprint.PrintOpenWhiskWarning(warningString) |
| |
| pkg.License = DEFAULT_PACKAGE_LICENSE |
| } else { |
| utils.CheckLicense(pkg.License) |
| } |
| |
| // package inputs are set as package inputs of type Parameter{} |
| // read all package inputs, interpolate their values using env. variables |
| // check if input variable itself is an env. variable |
| packageInputs, inputs, err := dm.composePackageInputs(projectInputs, pkg.Inputs, filePath) |
| if err != nil { |
| return nil, nil, err |
| } |
| if len(inputs) > 0 { |
| pag.Parameters = inputs |
| } |
| |
| // set Package Annotations |
| listOfAnnotations := dm.composeAnnotations(pkg.Annotations) |
| if len(listOfAnnotations) > 0 { |
| pag.Annotations = append(pag.Annotations, listOfAnnotations...) |
| } |
| |
| // add Managed Annotations if this is Managed Deployment |
| if utils.Flags.Managed || utils.Flags.Sync { |
| pag.Annotations = append(pag.Annotations, managedAnnotations) |
| } |
| |
| // "default" package is a reserved package name |
| // and in this case wskdeploy deploys openwhisk entities under |
| // /namespace instead of /namespace/package |
| if strings.ToLower(pag.Name) == DEFAULT_PACKAGE { |
| wskprint.PrintlnOpenWhiskVerbose(utils.Flags.Verbose, wski18n.T(wski18n.ID_MSG_DEFAULT_PACKAGE)) |
| // when a package is marked public with "Public: true" in manifest file |
| // the package is visible to anyone and created with publish state |
| // set to shared otherwise publish state is set to private |
| } else if pkg.Public { |
| warningMsg := wski18n.T(wski18n.ID_WARN_PACKAGE_IS_PUBLIC_X_package_X, |
| map[string]interface{}{ |
| wski18n.KEY_PACKAGE: pag.Name}) |
| wskprint.PrintlnOpenWhiskWarning(warningMsg) |
| pag.Publish = &(pkg.Public) |
| } |
| |
| return pag, packageInputs, nil |
| } |
| |
| func (dm *YAMLParser) ComposeSequencesFromAllPackages(namespace string, mani *YAML, manifestFilePath string, managedAnnotations whisk.KeyValue, packageInputs map[string]PackageInputs) ([]utils.ActionRecord, error) { |
| var sequences []utils.ActionRecord = make([]utils.ActionRecord, 0) |
| manifestPackages := make(map[string]Package) |
| |
| if len(mani.Packages) != 0 { |
| manifestPackages = mani.Packages |
| } else { |
| manifestPackages = mani.GetProject().Packages |
| } |
| |
| for n, p := range manifestPackages { |
| s, err := dm.ComposeSequences(namespace, p.Sequences, n, manifestFilePath, managedAnnotations, packageInputs[n]) |
| if err == nil { |
| sequences = append(sequences, s...) |
| } else { |
| return nil, err |
| } |
| } |
| return sequences, nil |
| } |
| |
| func (dm *YAMLParser) ComposeSequences(namespace string, sequences map[string]Sequence, packageName string, manifestFilePath string, managedAnnotations whisk.KeyValue, packageInputs PackageInputs) ([]utils.ActionRecord, error) { |
| var listOfSequences []utils.ActionRecord = make([]utils.ActionRecord, 0) |
| var errorParser error |
| |
| for key, sequence := range sequences { |
| wskaction := new(whisk.Action) |
| wskaction.Exec = new(whisk.Exec) |
| wskaction.Exec.Kind = YAML_KEY_SEQUENCE |
| actionList := strings.Split(sequence.Actions, ",") |
| |
| var components []string |
| for _, a := range actionList { |
| act := strings.TrimSpace(a) |
| if !strings.ContainsRune(act, []rune(PATH_SEPARATOR)[0]) && !strings.HasPrefix(act, packageName+PATH_SEPARATOR) && |
| strings.ToLower(packageName) != DEFAULT_PACKAGE { |
| act = path.Join(packageName, act) |
| } |
| components = append(components, path.Join(PATH_SEPARATOR+namespace, act)) |
| } |
| |
| wskaction.Exec.Components = components |
| if i, ok := packageInputs.Inputs[wskenv.GetEnvVarName(key)]; ok { |
| wskaction.Name = i.Value.(string) |
| } else { |
| wskaction.Name = wskenv.ConvertSingleName(key) |
| } |
| pub := false |
| wskaction.Publish = &pub |
| wskaction.Namespace = namespace |
| |
| annotations := dm.composeAnnotations(sequence.Annotations) |
| if len(annotations) > 0 { |
| wskaction.Annotations = annotations |
| } |
| |
| // appending managed annotations if its a managed deployment |
| if utils.Flags.Managed || utils.Flags.Sync { |
| wskaction.Annotations = append(wskaction.Annotations, managedAnnotations) |
| } |
| |
| // Web Export |
| // Treat sequence as a web action, a raw HTTP web action, or as a standard action based on web-export; |
| // when web-export is set to yes | true, treat sequence as a web action, |
| // when web-export is set to raw, treat sequence as a raw HTTP web action, |
| // when web-export is set to no | false, treat sequence as a standard action |
| if len(sequence.Web) != 0 { |
| wskaction.Annotations, errorParser = webaction.SetWebActionAnnotations(manifestFilePath, wskaction.Name, sequence.Web, wskaction.Annotations, false) |
| if errorParser != nil { |
| return nil, errorParser |
| } |
| } |
| |
| // TODO Add web-secure support for sequences? |
| |
| record := utils.ActionRecord{Action: wskaction, Packagename: packageName, Filepath: key} |
| listOfSequences = append(listOfSequences, record) |
| } |
| return listOfSequences, nil |
| } |
| |
| func (dm *YAMLParser) ComposeActionsFromAllPackages(manifest *YAML, filePath string, managedAnnotations whisk.KeyValue, packageInputs map[string]PackageInputs) ([]utils.ActionRecord, error) { |
| var actions []utils.ActionRecord = make([]utils.ActionRecord, 0) |
| manifestPackages := make(map[string]Package) |
| |
| if len(manifest.Packages) != 0 { |
| manifestPackages = manifest.Packages |
| } else { |
| manifestPackages = manifest.GetProject().Packages |
| } |
| |
| for n, p := range manifestPackages { |
| a, err := dm.ComposeActions(filePath, p.Actions, n, managedAnnotations, packageInputs[n]) |
| if err == nil { |
| actions = append(actions, a...) |
| } else { |
| return nil, err |
| } |
| } |
| return actions, nil |
| } |
| |
| func (dm *YAMLParser) validateActionCode(manifestFilePath string, action Action) error { |
| // Check if action.Function is specified with action.Code |
| // with action.Code, action.Function is not allowed |
| // with action.Code, action.Runtime should be specified |
| if len(action.Function) != 0 { |
| err := wski18n.T(wski18n.ID_ERR_ACTION_INVALID_X_action_X, |
| map[string]interface{}{ |
| wski18n.KEY_ACTION: action.Name}) |
| return wskderrors.NewYAMLFileFormatError(manifestFilePath, err) |
| } |
| if len(action.Runtime) == 0 { |
| err := wski18n.T(wski18n.ID_ERR_ACTION_MISSING_RUNTIME_WITH_CODE_X_action_X, |
| map[string]interface{}{ |
| wski18n.KEY_ACTION: action.Name}) |
| return wskderrors.NewYAMLFileFormatError(manifestFilePath, err) |
| } |
| return nil |
| } |
| |
| func (dm *YAMLParser) readActionCode(manifestFilePath string, action Action) (*whisk.Exec, error) { |
| exec := new(whisk.Exec) |
| if err := dm.validateActionCode(manifestFilePath, action); err != nil { |
| return nil, err |
| } |
| // validate runtime from the manifest file |
| // error out if the specified runtime is not valid or not supported |
| // even if runtime is invalid, deploy action with specified runtime in strict mode |
| if utils.Flags.Strict { |
| exec.Kind = action.Runtime |
| } else if runtimes.CheckExistRuntime(action.Runtime, runtimes.SupportedRunTimes) { |
| exec.Kind = action.Runtime |
| } else if len(runtimes.DefaultRunTimes[action.Runtime]) != 0 { |
| exec.Kind = runtimes.DefaultRunTimes[action.Runtime] |
| } else { |
| err := wski18n.T(wski18n.ID_ERR_RUNTIME_INVALID_X_runtime_X_action_X, |
| map[string]interface{}{ |
| wski18n.KEY_RUNTIME: action.Runtime, |
| wski18n.KEY_ACTION: action.Name}) |
| return nil, wskderrors.NewYAMLFileFormatError(manifestFilePath, err) |
| } |
| exec.Code = &(action.Code) |
| // we can specify the name of the action entry point using main |
| if len(action.Main) != 0 { |
| exec.Main = action.Main |
| } |
| return exec, nil |
| } |
| |
| func (dm *YAMLParser) validateActionFunction(manifestFileName string, action Action, ext string, kind string) error { |
| // produce an error when a runtime could not be derived from the action file extension |
| // and its not explicitly specified in the manifest YAML file |
| // and action source is not a zip file |
| if len(action.Runtime) == 0 && len(action.Docker) == 0 && !action.Native { |
| if ext == runtimes.ZIP_FILE_EXTENSION { |
| errMessage := wski18n.T(wski18n.ID_ERR_RUNTIME_INVALID_X_runtime_X_action_X, |
| map[string]interface{}{ |
| wski18n.KEY_RUNTIME: runtimes.RUNTIME_NOT_SPECIFIED, |
| wski18n.KEY_ACTION: action.Name}) |
| return wskderrors.NewInvalidRuntimeError(errMessage, |
| manifestFileName, |
| action.Name, |
| runtimes.RUNTIME_NOT_SPECIFIED, |
| runtimes.ListOfSupportedRuntimes(runtimes.SupportedRunTimes)) |
| } else if len(kind) == 0 { |
| errMessage := wski18n.T(wski18n.ID_ERR_RUNTIME_ACTION_SOURCE_NOT_SUPPORTED_X_ext_X_action_X, |
| map[string]interface{}{ |
| wski18n.KEY_EXTENSION: ext, |
| wski18n.KEY_ACTION: action.Name}) |
| return wskderrors.NewInvalidRuntimeError(errMessage, |
| manifestFileName, |
| action.Name, |
| runtimes.RUNTIME_NOT_SPECIFIED, |
| runtimes.ListOfSupportedRuntimes(runtimes.SupportedRunTimes)) |
| } |
| } |
| return nil |
| } |
| |
| func (dm *YAMLParser) readActionFunction(manifestFilePath string, manifestFileName string, action Action) (string, *whisk.Exec, error) { |
| var actionFilePath string |
| var zipFileName string |
| exec := new(whisk.Exec) |
| f := wskenv.InterpolateStringWithEnvVar(action.Function) |
| interpolatedActionFunction := f.(string) |
| |
| // check if action function is pointing to an URL |
| // we do not support if function is pointing to remote directory |
| // therefore error out if there is a combination of http/https ending in a directory |
| if strings.HasPrefix(interpolatedActionFunction, HTTP) || strings.HasPrefix(interpolatedActionFunction, HTTPS) { |
| if len(path.Ext(interpolatedActionFunction)) == 0 { |
| err := wski18n.T(wski18n.ID_ERR_ACTION_FUNCTION_REMOTE_DIR_NOT_SUPPORTED_X_action_X_url_X, |
| map[string]interface{}{ |
| wski18n.KEY_ACTION: action.Name, |
| wski18n.KEY_URL: interpolatedActionFunction}) |
| return actionFilePath, nil, wskderrors.NewYAMLFileFormatError(manifestFilePath, err) |
| } |
| actionFilePath = interpolatedActionFunction |
| } else { |
| actionFilePath = strings.TrimRight(manifestFilePath, manifestFileName) + interpolatedActionFunction |
| } |
| |
| if utils.IsDirectory(actionFilePath) { |
| zipFileName = actionFilePath + "." + runtimes.ZIP_FILE_EXTENSION |
| err := utils.NewZipWritter(actionFilePath, zipFileName, action.Include, action.Exclude, filepath.Dir(manifestFilePath)).Zip() |
| if err != nil { |
| return actionFilePath, nil, err |
| } |
| defer os.Remove(zipFileName) |
| actionFilePath = zipFileName |
| } |
| |
| action.Function = actionFilePath |
| |
| // determine extension of the given action source file |
| ext := filepath.Ext(actionFilePath) |
| // drop the "." from file extension |
| if len(ext) > 0 && ext[0] == '.' { |
| ext = ext[1:] |
| } |
| |
| // determine default runtime for the given file extension |
| var kind string |
| r := runtimes.FileExtensionRuntimeKindMap[ext] |
| kind = runtimes.DefaultRunTimes[r] |
| if err := dm.validateActionFunction(manifestFileName, action, ext, kind); err != nil { |
| return actionFilePath, nil, err |
| } |
| exec.Kind = kind |
| |
| dat, err := utils.Read(actionFilePath) |
| if err != nil { |
| return actionFilePath, nil, err |
| } |
| code := string(dat) |
| if ext == runtimes.ZIP_FILE_EXTENSION || ext == runtimes.JAR_FILE_EXTENSION { |
| code = base64.StdEncoding.EncodeToString([]byte(dat)) |
| } |
| exec.Code = &code |
| |
| /* |
| * Action.Runtime |
| * Perform few checks if action runtime is specified in manifest YAML file |
| * (1) Check if specified runtime is one of the supported runtimes by OpenWhisk server |
| * (2) Check if specified runtime is consistent with action source file extensions |
| * Set the action runtime to match with the source file extension, if wskdeploy is not invoked in strict mode |
| */ |
| if len(action.Runtime) != 0 { |
| if runtimes.CheckExistRuntime(action.Runtime, runtimes.SupportedRunTimes) { |
| // for zip actions, rely on the runtimes from the manifest file as it can not be derived from the action source file extension |
| // pick runtime from manifest file if its supported by OpenWhisk server |
| if ext == runtimes.ZIP_FILE_EXTENSION { |
| exec.Kind = action.Runtime |
| } else { |
| if runtimes.CheckRuntimeConsistencyWithFileExtension(ext, action.Runtime) { |
| exec.Kind = action.Runtime |
| } else { |
| warnStr := wski18n.T(wski18n.ID_ERR_RUNTIME_MISMATCH_X_runtime_X_ext_X_action_X, |
| map[string]interface{}{ |
| wski18n.KEY_RUNTIME: action.Runtime, |
| wski18n.KEY_EXTENSION: ext, |
| wski18n.KEY_ACTION: action.Name}) |
| wskprint.PrintOpenWhiskWarning(warnStr) |
| |
| // even if runtime is not consistent with file extension, deploy action with specified runtime in strict mode |
| if utils.Flags.Strict { |
| exec.Kind = action.Runtime |
| } else { |
| warnStr := wski18n.T(wski18n.ID_WARN_RUNTIME_CHANGED_X_runtime_X_action_X, |
| map[string]interface{}{ |
| wski18n.KEY_RUNTIME: exec.Kind, |
| wski18n.KEY_ACTION: action.Name}) |
| wskprint.PrintOpenWhiskWarning(warnStr) |
| } |
| } |
| } |
| } else { |
| warnStr := wski18n.T(wski18n.ID_ERR_RUNTIME_INVALID_X_runtime_X_action_X, |
| map[string]interface{}{ |
| wski18n.KEY_RUNTIME: action.Runtime, |
| wski18n.KEY_ACTION: action.Name}) |
| wskprint.PrintOpenWhiskWarning(warnStr) |
| |
| if ext == runtimes.ZIP_FILE_EXTENSION { |
| // for zip action, error out if specified runtime is not supported by |
| // OpenWhisk server |
| return actionFilePath, nil, wskderrors.NewInvalidRuntimeError(warnStr, |
| manifestFileName, |
| action.Name, |
| action.Runtime, |
| runtimes.ListOfSupportedRuntimes(runtimes.SupportedRunTimes)) |
| } else { |
| if utils.Flags.Strict { |
| exec.Kind = action.Runtime |
| } else { |
| warnStr := wski18n.T(wski18n.ID_WARN_RUNTIME_CHANGED_X_runtime_X_action_X, |
| map[string]interface{}{ |
| wski18n.KEY_RUNTIME: exec.Kind, |
| wski18n.KEY_ACTION: action.Name}) |
| wskprint.PrintOpenWhiskWarning(warnStr) |
| } |
| } |
| |
| } |
| } |
| // we can specify the name of the action entry point using main |
| if len(action.Main) != 0 { |
| exec.Main = action.Main |
| } |
| |
| return actionFilePath, exec, nil |
| } |
| |
| func (dm *YAMLParser) composeActionExec(manifestFilePath string, manifestFileName string, action Action) (string, *whisk.Exec, error) { |
| var actionFilePath string |
| exec := new(whisk.Exec) |
| var err error |
| |
| if len(action.Code) != 0 { |
| exec, err = dm.readActionCode(manifestFilePath, action) |
| if err != nil { |
| return actionFilePath, nil, err |
| } |
| } |
| if len(action.Function) != 0 { |
| actionFilePath, exec, err = dm.readActionFunction(manifestFilePath, manifestFileName, action) |
| if err != nil { |
| return actionFilePath, nil, err |
| } |
| } |
| |
| // when an action has Docker image specified, |
| // set exec.Kind to "blackbox" and |
| // set exec.Image to specified image e.g. dockerhub/image |
| // when an action Native is set to true, |
| // set exec.Image to openwhisk/skeleton |
| if len(action.Docker) != 0 || action.Native { |
| exec.Kind = runtimes.BLACKBOX |
| if action.Native { |
| exec.Image = NATIVE_DOCKER_IMAGE |
| } else { |
| exec.Image = wskenv.InterpolateStringWithEnvVar(action.Docker).(string) |
| } |
| } |
| |
| return actionFilePath, exec, err |
| } |
| |
| func (dm *YAMLParser) validateActionLimits(limits Limits) { |
| // TODO() use LIMITS_UNSUPPORTED in yamlparser to enumerate through instead of hardcoding |
| // emit warning errors if these limits are not nil |
| utils.NotSupportLimits(limits.ConcurrentActivations, LIMIT_VALUE_CONCURRENT_ACTIVATIONS) |
| utils.NotSupportLimits(limits.UserInvocationRate, LIMIT_VALUE_USER_INVOCATION_RATE) |
| utils.NotSupportLimits(limits.CodeSize, LIMIT_VALUE_CODE_SIZE) |
| utils.NotSupportLimits(limits.ParameterSize, LIMIT_VALUE_PARAMETER_SIZE) |
| } |
| |
| func (dm *YAMLParser) composeActionLimits(limits Limits) *whisk.Limits { |
| dm.validateActionLimits(limits) |
| wsklimits := new(whisk.Limits) |
| for _, t := range LIMITS_SUPPORTED { |
| switch t { |
| case LIMIT_VALUE_TIMEOUT: |
| if utils.LimitsTimeoutValidation(limits.Timeout) { |
| wsklimits.Timeout = limits.Timeout |
| } else { |
| warningString := wski18n.T(wski18n.ID_WARN_LIMIT_IGNORED_X_limit_X, |
| map[string]interface{}{wski18n.KEY_LIMIT: LIMIT_VALUE_TIMEOUT}) |
| wskprint.PrintOpenWhiskWarning(warningString) |
| } |
| case LIMIT_VALUE_MEMORY_SIZE: |
| if utils.LimitsMemoryValidation(limits.Memory) { |
| wsklimits.Memory = limits.Memory |
| } else { |
| warningString := wski18n.T(wski18n.ID_WARN_LIMIT_IGNORED_X_limit_X, |
| map[string]interface{}{wski18n.KEY_LIMIT: LIMIT_VALUE_MEMORY_SIZE}) |
| wskprint.PrintOpenWhiskWarning(warningString) |
| |
| } |
| case LIMIT_VALUE_LOG_SIZE: |
| if utils.LimitsLogsizeValidation(limits.Logsize) { |
| wsklimits.Logsize = limits.Logsize |
| } else { |
| warningString := wski18n.T(wski18n.ID_WARN_LIMIT_IGNORED_X_limit_X, |
| map[string]interface{}{wski18n.KEY_LIMIT: LIMIT_VALUE_LOG_SIZE}) |
| wskprint.PrintOpenWhiskWarning(warningString) |
| |
| } |
| } |
| } |
| if wsklimits.Timeout != nil || wsklimits.Memory != nil || wsklimits.Logsize != nil { |
| return wsklimits |
| } |
| return nil |
| } |
| |
| func (dm *YAMLParser) warnIfRedundantWebActionFlags(action Action) { |
| // Warn user if BOTH web and web-export specified, |
| // as they are redundant; defer to "web" flag and its value |
| if len(action.Web) != 0 && len(action.WebExport) != 0 { |
| warningString := wski18n.T(wski18n.ID_WARN_ACTION_WEB_X_action_X, |
| map[string]interface{}{wski18n.KEY_ACTION: action.Name}) |
| wskprint.PrintOpenWhiskWarning(warningString) |
| } |
| } |
| |
| func (dm *YAMLParser) ComposeActions(manifestFilePath string, actions map[string]Action, packageName string, managedAnnotations whisk.KeyValue, packageInputs PackageInputs) ([]utils.ActionRecord, error) { |
| |
| var errorParser error |
| var listOfActions []utils.ActionRecord = make([]utils.ActionRecord, 0) |
| splitManifestFilePath := strings.Split(manifestFilePath, string(PATH_SEPARATOR)) |
| manifestFileName := splitManifestFilePath[len(splitManifestFilePath)-1] |
| |
| for actionName, action := range actions { |
| var actionFilePath string |
| |
| // update the action (of type Action) to set its name |
| // here key name is the action name |
| action.Name = actionName |
| |
| // Create action data object from client library |
| wskaction := new(whisk.Action) |
| |
| //set action.Function to action.Location |
| //because Location is deprecated in Action entity |
| if len(action.Function) == 0 && len(action.Location) != 0 { |
| action.Function = action.Location |
| } |
| |
| actionFilePath, wskaction.Exec, errorParser = dm.composeActionExec(manifestFilePath, manifestFileName, action) |
| if errorParser != nil { |
| return nil, errorParser |
| } |
| |
| // Action.Inputs |
| listOfInputs, err := dm.composeInputs(action.Inputs, packageInputs, manifestFilePath) |
| if err != nil { |
| return nil, err |
| } |
| if len(listOfInputs) > 0 { |
| wskaction.Parameters = listOfInputs |
| } |
| |
| // Action.Outputs |
| // TODO{} add outputs as annotations (work to discuss officially supporting for compositions) |
| //listOfOutputs, err := dm.composeOutputs(action.Outputs, manifestFilePath) |
| //if err != nil { |
| // return nil, err |
| //} |
| //if len(listOfOutputs) > 0 { |
| // wskaction.Annotations = listOfOutputs |
| //} |
| |
| // Action.Annotations |
| // ================== |
| // WARNING! Processing of explicit Annotations MUST occur before handling of Action keys, as these |
| // keys often need to check for inconsistencies (and raise errors). |
| if listOfAnnotations := dm.composeAnnotations(action.Annotations); len(listOfAnnotations) > 0 { |
| wskaction.Annotations = append(wskaction.Annotations, listOfAnnotations...) |
| } |
| |
| // add managed annotations if its marked as managed deployment |
| if utils.Flags.Managed || utils.Flags.Sync { |
| wskaction.Annotations = append(wskaction.Annotations, managedAnnotations) |
| } |
| |
| // Web Export (i.e., "web-export" annotation) |
| // ========== |
| // Treat ACTION as a web action, a raw HTTP web action, or as a standard action based on web-export; |
| // when web-export is set to yes | true, treat action as a web action, |
| // when web-export is set to raw, treat action as a raw HTTP web action, |
| // when web-export is set to no | false, treat action as a standard action |
| dm.warnIfRedundantWebActionFlags(action) |
| if len(action.GetWeb()) != 0 { |
| wskaction.Annotations, errorParser = webaction.SetWebActionAnnotations( |
| manifestFilePath, |
| action.Name, |
| action.GetWeb(), |
| wskaction.Annotations, |
| false) |
| if errorParser != nil { |
| return listOfActions, errorParser |
| } |
| } |
| |
| // validate special action annotations such as "require-whisk-auth" |
| // TODO: the Manifest parser will validate any declared APIs that ref. this action |
| if wskaction.Annotations != nil { |
| if webaction.HasAnnotation(&wskaction.Annotations, webaction.REQUIRE_WHISK_AUTH) { |
| _, errorParser = webaction.ValidateRequireWhiskAuthAnnotationValue( |
| actionName, |
| wskaction.Annotations.GetValue(webaction.REQUIRE_WHISK_AUTH)) |
| } |
| if errorParser != nil { |
| return listOfActions, errorParser |
| } |
| } |
| |
| // Action.Limits |
| if action.Limits != nil { |
| if wsklimits := dm.composeActionLimits(*(action.Limits)); wsklimits != nil { |
| wskaction.Limits = wsklimits |
| } |
| } |
| |
| // Conductor Action |
| if action.Conductor { |
| wskaction.Annotations = append(wskaction.Annotations, conductor.ConductorAction()) |
| } |
| |
| // Set other top-level values for the action (e.g., name, version, publish, etc.) |
| wskaction.Name = actionName |
| pub := false |
| wskaction.Publish = &pub |
| wskaction.Version = wskenv.ConvertSingleName(action.Version) |
| |
| // create a "record" of the Action relative to its package and function filepath |
| // which will be used to compose the REST API calls |
| record := utils.ActionRecord{Action: wskaction, Packagename: packageName, Filepath: actionFilePath} |
| listOfActions = append(listOfActions, record) |
| } |
| |
| return listOfActions, nil |
| } |
| |
| func (dm *YAMLParser) ComposeTriggersFromAllPackages(manifest *YAML, filePath string, managedAnnotations whisk.KeyValue, inputs map[string]PackageInputs) ([]*whisk.Trigger, error) { |
| var triggers []*whisk.Trigger = make([]*whisk.Trigger, 0) |
| manifestPackages := make(map[string]Package) |
| |
| if len(manifest.Packages) != 0 { |
| manifestPackages = manifest.Packages |
| } else { |
| manifestPackages = manifest.GetProject().Packages |
| } |
| |
| for packageName, pkg := range manifestPackages { |
| t, err := dm.ComposeTriggers(filePath, pkg, managedAnnotations, inputs[packageName]) |
| if err == nil { |
| triggers = append(triggers, t...) |
| } else { |
| return nil, err |
| } |
| } |
| return triggers, nil |
| } |
| |
| func (dm *YAMLParser) ComposeTriggers(filePath string, pkg Package, managedAnnotations whisk.KeyValue, packageInputs PackageInputs) ([]*whisk.Trigger, error) { |
| var errorParser error |
| var listOfTriggers []*whisk.Trigger = make([]*whisk.Trigger, 0) |
| |
| for _, trigger := range pkg.GetTriggerList() { |
| wsktrigger := new(whisk.Trigger) |
| if i, ok := packageInputs.Inputs[wskenv.GetEnvVarName(trigger.Name)]; ok { |
| wsktrigger.Name = i.Value.(string) |
| } else { |
| wsktrigger.Name = wskenv.ConvertSingleName(trigger.Name) |
| } |
| wsktrigger.Namespace = trigger.Namespace |
| pub := false |
| wsktrigger.Publish = &pub |
| |
| // print warning information when .Source key's value is not empty |
| if len(trigger.Source) != 0 { |
| warningString := wski18n.T( |
| wski18n.ID_WARN_KEY_DEPRECATED_X_oldkey_X_filetype_X_newkey_X, |
| map[string]interface{}{ |
| wski18n.KEY_OLD: YAML_KEY_SOURCE, |
| wski18n.KEY_NEW: YAML_KEY_FEED, |
| wski18n.KEY_FILE_TYPE: wski18n.MANIFEST_FILE}) |
| wskprint.PrintOpenWhiskWarning(warningString) |
| } |
| if len(trigger.Feed) == 0 { |
| trigger.Feed = trigger.Source |
| } |
| |
| // replacing env. variables here in the trigger feed name |
| // to support trigger feed with $READ_FROM_ENV_TRIGGER_FEED |
| trigger.Feed = wskenv.ConvertSingleName(trigger.Feed) |
| |
| keyValArr := make(whisk.KeyValueArr, 0) |
| if len(trigger.Feed) != 0 { |
| var keyVal whisk.KeyValue |
| keyVal.Key = YAML_KEY_FEED |
| keyVal.Value = trigger.Feed |
| keyValArr = append(keyValArr, keyVal) |
| wsktrigger.Annotations = keyValArr |
| } |
| |
| inputs, err := dm.composeInputs(trigger.Inputs, packageInputs, filePath) |
| if err != nil { |
| return nil, errorParser |
| } |
| if len(inputs) > 0 { |
| wsktrigger.Parameters = inputs |
| } |
| |
| listOfAnnotations := dm.composeAnnotations(trigger.Annotations) |
| if len(listOfAnnotations) > 0 { |
| wsktrigger.Annotations = append(wsktrigger.Annotations, listOfAnnotations...) |
| } |
| |
| // add managed annotations if its a managed deployment |
| if utils.Flags.Managed || utils.Flags.Sync { |
| wsktrigger.Annotations = append(wsktrigger.Annotations, managedAnnotations) |
| } |
| |
| listOfTriggers = append(listOfTriggers, wsktrigger) |
| } |
| return listOfTriggers, nil |
| } |
| |
| func (dm *YAMLParser) ComposeRulesFromAllPackages(manifest *YAML, managedAnnotations whisk.KeyValue, packageInputs map[string]PackageInputs) ([]*whisk.Rule, error) { |
| var rules []*whisk.Rule = make([]*whisk.Rule, 0) |
| manifestPackages := make(map[string]Package) |
| |
| if len(manifest.Packages) != 0 { |
| manifestPackages = manifest.Packages |
| } else { |
| manifestPackages = manifest.GetProject().Packages |
| } |
| |
| for n, p := range manifestPackages { |
| r, err := dm.ComposeRules(p, n, managedAnnotations, packageInputs[n]) |
| if err == nil { |
| rules = append(rules, r...) |
| } else { |
| return nil, err |
| } |
| } |
| return rules, nil |
| } |
| |
| func (dm *YAMLParser) ComposeRules(pkg Package, packageName string, managedAnnotations whisk.KeyValue, packageInputs PackageInputs) ([]*whisk.Rule, error) { |
| var rules []*whisk.Rule = make([]*whisk.Rule, 0) |
| |
| for _, rule := range pkg.GetRuleList() { |
| wskrule := new(whisk.Rule) |
| if i, ok := packageInputs.Inputs[wskenv.GetEnvVarName(rule.Name)]; ok { |
| wskrule.Name = i.Value.(string) |
| } else { |
| wskrule.Name = wskenv.ConvertSingleName(rule.Name) |
| } |
| //wskrule.Namespace = rule.Namespace |
| pub := false |
| wskrule.Publish = &pub |
| if i, ok := packageInputs.Inputs[wskenv.GetEnvVarName(rule.Trigger)]; ok { |
| wskrule.Trigger = i.Value.(string) |
| } else { |
| wskrule.Trigger = wskenv.ConvertSingleName(rule.Trigger) |
| } |
| if i, ok := packageInputs.Inputs[wskenv.GetEnvVarName(rule.Action)]; ok { |
| wskrule.Action = i.Value.(string) |
| } else { |
| wskrule.Action = wskenv.ConvertSingleName(rule.Action) |
| } |
| act := strings.TrimSpace(wskrule.Action.(string)) |
| if !strings.ContainsRune(act, []rune(PATH_SEPARATOR)[0]) && !strings.HasPrefix(act, packageName+PATH_SEPARATOR) && |
| strings.ToLower(packageName) != DEFAULT_PACKAGE { |
| act = path.Join(packageName, act) |
| } |
| wskrule.Action = act |
| listOfAnnotations := dm.composeAnnotations(rule.Annotations) |
| if len(listOfAnnotations) > 0 { |
| wskrule.Annotations = append(wskrule.Annotations, listOfAnnotations...) |
| } |
| |
| // add managed annotations if its a managed deployment |
| if utils.Flags.Managed || utils.Flags.Sync { |
| wskrule.Annotations = append(wskrule.Annotations, managedAnnotations) |
| } |
| |
| rules = append(rules, wskrule) |
| } |
| return rules, nil |
| } |
| |
| func (dm *YAMLParser) ComposeApiRecordsFromAllPackages(client *whisk.Config, manifest *YAML, |
| actionrecords []utils.ActionRecord, |
| sequencerecords []utils.ActionRecord) ([]*whisk.ApiCreateRequest, map[string]*whisk.ApiCreateRequestOptions, error) { |
| var requests = make([]*whisk.ApiCreateRequest, 0) |
| var responses = make(map[string]*whisk.ApiCreateRequestOptions, 0) |
| manifestPackages := make(map[string]Package) |
| |
| if len(manifest.Packages) != 0 { |
| manifestPackages = manifest.Packages |
| } else { |
| manifestPackages = manifest.GetProject().Packages |
| } |
| |
| for packageName, p := range manifestPackages { |
| r, response, err := dm.ComposeApiRecords(client, packageName, p, manifest.Filepath, |
| actionrecords, sequencerecords) |
| if err == nil { |
| requests = append(requests, r...) |
| for k, v := range response { |
| responses[k] = v |
| } |
| } else { |
| return nil, nil, err |
| } |
| } |
| return requests, responses, nil |
| } |
| |
| /* |
| * read API section from manifest file: |
| * apis: # List of APIs |
| * hello-world: #API name |
| * /hello: #gateway base path |
| * /world: #gateway rel path |
| * greeting: get #action name: gateway method |
| * |
| * compose APIDoc structure from the manifest: |
| * { |
| * "apidoc":{ |
| * "namespace":<namespace>, |
| * "gatewayBasePath":"/hello", |
| * "gatewayPath":"/world", |
| * "gatewayMethod":"GET", |
| * "action":{ |
| * "name":"hello", |
| * "namespace":"guest", |
| * "backendMethod":"GET", |
| * "backendUrl":<url>, |
| * "authkey":<auth> |
| * } |
| * } |
| * } |
| */ |
| func (dm *YAMLParser) ComposeApiRecords(client *whisk.Config, packageName string, pkg Package, manifestPath string, |
| actionrecords []utils.ActionRecord, sequencerecords []utils.ActionRecord) ([]*whisk.ApiCreateRequest, map[string]*whisk.ApiCreateRequestOptions, error) { |
| var requests = make([]*whisk.ApiCreateRequest, 0) |
| |
| // supply a dummy API GW token as it is optional |
| if pkg.Apis != nil && len(pkg.Apis) != 0 { |
| if len(client.ApigwAccessToken) == 0 { |
| warningString := wski18n.T(wski18n.ID_MSG_CONFIG_PROVIDE_DEFAULT_APIGW_ACCESS_TOKEN, |
| map[string]interface{}{wski18n.KEY_DUMMY_TOKEN: DUMMY_APIGW_ACCESS_TOKEN}) |
| wskprint.PrintOpenWhiskWarning(warningString) |
| client.ApigwAccessToken = DUMMY_APIGW_ACCESS_TOKEN |
| } |
| } |
| |
| requestOptions := make(map[string]*whisk.ApiCreateRequestOptions, 0) |
| |
| for apiName, apiDoc := range pkg.Apis { |
| for gatewayBasePath, gatewayBasePathMap := range apiDoc { |
| // Base Path |
| // validate base path should not have any path parameters |
| if !isGatewayBasePathValid(gatewayBasePath) { |
| err := wskderrors.NewYAMLParserErr(manifestPath, |
| wski18n.T(wski18n.ID_ERR_API_GATEWAY_BASE_PATH_INVALID_X_api_X, |
| map[string]interface{}{wski18n.KEY_API_BASE_PATH: gatewayBasePath})) |
| return requests, requestOptions, err |
| } |
| // append "/" to the gateway base path if its missing |
| if !strings.HasPrefix(gatewayBasePath, PATH_SEPARATOR) { |
| gatewayBasePath = PATH_SEPARATOR + gatewayBasePath |
| } |
| for gatewayRelPath, gatewayRelPathMap := range gatewayBasePathMap { |
| // Relative Path |
| // append "/" to the gateway relative path if its missing |
| if !strings.HasPrefix(gatewayRelPath, PATH_SEPARATOR) { |
| gatewayRelPath = PATH_SEPARATOR + gatewayRelPath |
| } |
| for actionName, gatewayMethodResponse := range gatewayRelPathMap { |
| // verify that the action is defined under action records |
| if _, ok := pkg.Actions[actionName]; ok { |
| // verify that the action is defined as web action; |
| // web or web-export set to any of [true, yes, raw]; if not, |
| // we will try to add it (if no strict" flag) and warn user that we did so |
| if err := webaction.TryUpdateAPIsActionToWebAction(actionrecords, packageName, |
| apiName, actionName, false); err != nil { |
| return requests, requestOptions, err |
| } |
| // verify that the sequence action is defined under sequence records |
| } else if _, ok := pkg.Sequences[actionName]; ok { |
| // verify that the sequence action is defined as web sequence |
| // web or web-export set to any of [true, yes, raw]; if not, |
| // we will try to add it (if no strict" flag) and warn user that we did so |
| if err := webaction.TryUpdateAPIsActionToWebAction(sequencerecords, packageName, |
| apiName, actionName, true); err != nil { |
| return requests, requestOptions, err |
| } |
| } else { |
| return nil, nil, wskderrors.NewYAMLFileFormatError(manifestPath, |
| wski18n.T(wski18n.ID_ERR_API_MISSING_ACTION_OR_SEQUENCE_X_action_or_sequence_X_api_X, |
| map[string]interface{}{ |
| wski18n.KEY_ACTION: actionName, |
| wski18n.KEY_API: apiName})) |
| } |
| |
| // get the list of path parameters from relative path |
| pathParameters := generatePathParameters(gatewayRelPath) |
| |
| // Check if response type is set to http for API using path parameters |
| if strings.ToLower(gatewayMethodResponse.Method) != utils.HTTP_FILE_EXTENSION && |
| len(pathParameters) > 0 { |
| warningString := wski18n.T(wski18n.ID_WARN_API_INVALID_RESPONSE_TYPE, |
| map[string]interface{}{ |
| wski18n.KEY_API: apiName, |
| wski18n.KEY_API_RELATIVE_PATH: gatewayRelPath, |
| wski18n.KEY_RESPONSE: gatewayMethodResponse.Response}) |
| wskprint.PrintlnOpenWhiskWarning(warningString) |
| gatewayMethodResponse.Response = utils.HTTP_FILE_EXTENSION |
| } |
| |
| // Check if API verb is valid, it must be one of (GET, PUT, POST, DELETE) |
| if _, ok := whisk.ApiVerbs[strings.ToUpper(gatewayMethodResponse.Method)]; !ok { |
| return nil, nil, wskderrors.NewInvalidAPIGatewayMethodError(manifestPath, |
| gatewayBasePath+gatewayRelPath, |
| gatewayMethodResponse.Method, |
| dm.getGatewayMethods()) |
| } |
| |
| apiDocActionName := actionName |
| if strings.ToLower(packageName) != DEFAULT_PACKAGE { |
| apiDocActionName = packageName + PATH_SEPARATOR + actionName |
| } |
| |
| // set action of an API Doc |
| apiDocAction := whisk.ApiAction{ |
| Name: apiDocActionName, |
| Namespace: client.Namespace, |
| BackendUrl: strings.Join([]string{HTTPS + |
| client.Host, strings.ToLower(API), |
| API_VERSION, WEB, client.Namespace, packageName, |
| actionName + "." + utils.HTTP_FILE_EXTENSION}, |
| PATH_SEPARATOR), |
| BackendMethod: gatewayMethodResponse.Method, |
| Auth: client.AuthToken, |
| } |
| |
| requestApiDoc := whisk.Api{ |
| GatewayBasePath: gatewayBasePath, |
| PathParameters: pathParameters, |
| GatewayRelPath: gatewayRelPath, |
| GatewayMethod: strings.ToUpper(gatewayMethodResponse.Method), |
| Namespace: client.Namespace, |
| ApiName: apiName, |
| Id: strings.Join([]string{API, client.Namespace, gatewayRelPath}, ":"), |
| Action: &apiDocAction, |
| } |
| |
| request := whisk.ApiCreateRequest{ |
| ApiDoc: &requestApiDoc, |
| } |
| |
| // add a newly created ApiCreateRequest object to a list of requests |
| requests = append(requests, &request) |
| |
| // Create an instance of ApiCreateRequestOptions |
| options := whisk.ApiCreateRequestOptions{ |
| ResponseType: gatewayMethodResponse.Response, |
| } |
| apiPath := request.ApiDoc.ApiName + " " + request.ApiDoc.GatewayBasePath + |
| request.ApiDoc.GatewayRelPath + " " + request.ApiDoc.GatewayMethod |
| requestOptions[apiPath] = &options |
| } |
| } |
| } |
| } |
| return requests, requestOptions, nil |
| } |
| |
| func (dm *YAMLParser) ComposeApiRecordsFromSwagger(client *whisk.Config, manifest *YAML) (*whisk.ApiCreateRequest, *whisk.ApiCreateRequestOptions, error) { |
| var api *whisk.Api |
| var err error |
| var config string = manifest.Project.Config |
| // Check to make sure the manifest has a config under project |
| if config == "" { |
| return nil, nil, nil |
| } |
| |
| api, err = dm.parseSwaggerApi(config, client.Namespace) |
| if err != nil { |
| whisk.Debug(whisk.DbgError, "parseSwaggerApi() error: %s\n", err) |
| errMsg := wski18n.T("Unable to parse swagger file: {{.err}}", map[string]interface{}{"err": err}) |
| whiskErr := whisk.MakeWskErrorFromWskError(errors.New(errMsg), err, whisk.EXIT_CODE_ERR_GENERAL, |
| whisk.DISPLAY_MSG, whisk.DISPLAY_USAGE) |
| return nil, nil, whiskErr |
| } |
| |
| apiCreateReq := new(whisk.ApiCreateRequest) |
| apiCreateReq.ApiDoc = api |
| |
| apiCreateReqOptions := new(whisk.ApiCreateRequestOptions) |
| |
| return apiCreateReq, apiCreateReqOptions, nil |
| |
| } |
| |
| /* |
| * Read the swagger config provided by under |
| * Project: |
| * config: swagger_filename.[yaml|yml] |
| * |
| * NOTE: This was lifted almost verbatim from openwhisk-cli/commands/api.go |
| * and as a follow up should probably be moved and updated to live in whiskclient-go |
| * NOTE: This does notb verify that actions used in swagger api definition are defined in |
| * the openwhisk server or manifest. |
| */ |
| func (dm *YAMLParser) parseSwaggerApi(configfile, namespace string) (*whisk.Api, error) { |
| if len(configfile) == 0 { |
| whisk.Debug(whisk.DbgError, "No swagger file is specified\n") |
| errMsg := wski18n.T("A swagger configuration file was not specified.") |
| whiskErr := whisk.MakeWskError(errors.New(errMsg), whisk.EXIT_CODE_ERR_GENERAL, |
| whisk.DISPLAY_MSG, whisk.DISPLAY_USAGE) |
| return nil, whiskErr |
| } |
| |
| swagger, err := ioutil.ReadFile(configfile) |
| if err != nil { |
| whisk.Debug(whisk.DbgError, "readFile(%s) error: %s\n", configfile, err) |
| errMsg := wski18n.T("Error reading swagger file '{{.name}}': {{.err}}", |
| map[string]interface{}{"name": configfile, "err": err}) |
| whiskErr := whisk.MakeWskErrorFromWskError(errors.New(errMsg), err, whisk.EXIT_CODE_ERR_GENERAL, |
| whisk.DISPLAY_MSG, whisk.DISPLAY_USAGE) |
| return nil, whiskErr |
| } |
| |
| // Check if this swagger is in JSON or YAML format |
| isYaml := strings.HasSuffix(configfile, YAML_FILE_EXTENSION) || strings.HasSuffix(configfile, YML_FILE_EXTENSION) |
| if isYaml { |
| whisk.Debug(whisk.DbgInfo, "Converting YAML formated API configuration into JSON\n") |
| jsonbytes, err := yamlHelper.YAMLToJSON([]byte(swagger)) |
| if err != nil { |
| whisk.Debug(whisk.DbgError, "yaml.YAMLToJSON() error: %s\n", err) |
| errMsg := wski18n.T("Unable to parse YAML configuration file: {{.err}}", map[string]interface{}{"err": err}) |
| whiskErr := whisk.MakeWskError(errors.New(errMsg), whisk.EXIT_CODE_ERR_GENERAL, |
| whisk.DISPLAY_MSG, whisk.NO_DISPLAY_USAGE) |
| return nil, whiskErr |
| } |
| swagger = jsonbytes |
| } |
| |
| // Parse the JSON into a swagger object |
| swaggerObj := new(whisk.ApiSwagger) |
| err = json.Unmarshal([]byte(swagger), swaggerObj) |
| if err != nil { |
| whisk.Debug(whisk.DbgError, "JSON parse of '%s' error: %s\n", configfile, err) |
| errMsg := wski18n.T("Error parsing swagger file '{{.name}}': {{.err}}", |
| map[string]interface{}{"name": configfile, "err": err}) |
| whiskErr := whisk.MakeWskErrorFromWskError(errors.New(errMsg), err, whisk.EXIT_CODE_ERR_GENERAL, |
| whisk.DISPLAY_MSG, whisk.DISPLAY_USAGE) |
| return nil, whiskErr |
| } |
| if swaggerObj.BasePath == "" || swaggerObj.SwaggerName == "" || swaggerObj.Info == nil || swaggerObj.Paths == nil { |
| whisk.Debug(whisk.DbgError, "Swagger file is invalid.\n") |
| errMsg := wski18n.T("Swagger file is invalid (missing basePath, info, paths, or swagger fields)") |
| whiskErr := whisk.MakeWskError(errors.New(errMsg), whisk.EXIT_CODE_ERR_GENERAL, |
| whisk.DISPLAY_MSG, whisk.DISPLAY_USAGE) |
| return nil, whiskErr |
| } |
| |
| if !strings.HasPrefix(swaggerObj.BasePath, "/") { |
| whisk.Debug(whisk.DbgError, "Swagger file basePath is invalid.\n") |
| errMsg := wski18n.T("Swagger file basePath must start with a leading slash (/)") |
| whiskErr := whisk.MakeWskError(errors.New(errMsg), whisk.EXIT_CODE_ERR_GENERAL, |
| whisk.DISPLAY_MSG, whisk.DISPLAY_USAGE) |
| return nil, whiskErr |
| } |
| |
| api := new(whisk.Api) |
| api.Namespace = namespace |
| api.Swagger = string(swagger) |
| |
| return api, nil |
| } |
| |
| func (dm *YAMLParser) getGatewayMethods() []string { |
| methods := []string{} |
| for k := range whisk.ApiVerbs { |
| methods = append(methods, k) |
| } |
| return methods |
| } |
| |
| func doesPathParamExist(params []string, param string) bool { |
| for _, e := range params { |
| if e == param { |
| return true |
| } |
| } |
| return false |
| } |
| |
| func getPathParameterNames(path string) []string { |
| var pathParameters []string |
| pathElements := strings.Split(path, PATH_SEPARATOR) |
| for _, e := range pathElements { |
| paramName := getParamName(e) |
| if len(paramName) > 0 { |
| if !doesPathParamExist(pathParameters, paramName) { |
| pathParameters = append(pathParameters, paramName) |
| } |
| } |
| } |
| return pathParameters |
| } |
| |
| func generatePathParameters(relativePath string) []whisk.ApiParameter { |
| pathParams := []whisk.ApiParameter{} |
| |
| pathParamNames := getPathParameterNames(relativePath) |
| for _, paramName := range pathParamNames { |
| param := whisk.ApiParameter{Name: paramName, In: "path", Required: true, Type: "string", |
| Description: wski18n.T("Default description for '{{.name}}'", map[string]interface{}{"name": paramName})} |
| pathParams = append(pathParams, param) |
| } |
| |
| return pathParams |
| } |
| |
| func isParam(param string) bool { |
| if strings.HasPrefix(param, PARAM_OPENING_BRACKET) && |
| strings.HasSuffix(param, PARAM_CLOSING_BRACKET) { |
| return true |
| } |
| return false |
| } |
| |
| func getParamName(param string) string { |
| paramName := "" |
| if isParam(param) { |
| paramName = param[1 : len(param)-1] |
| } |
| return paramName |
| } |
| |
| func isGatewayBasePathValid(basePath string) bool { |
| // return false if base path is empty string |
| if len(basePath) == 0 { |
| return false |
| } |
| // drop preceding "/" if exists |
| if strings.HasPrefix(basePath, PATH_SEPARATOR) { |
| basePath = basePath[1:] |
| } |
| // slice base path into substrings seperated by "/" |
| // if there are more than one substrings, basePath has path parameters |
| // basePath will have path parameters if substrings count is more than 1 |
| basePathElements := strings.Split(basePath, PATH_SEPARATOR) |
| if len(basePathElements) > 1 { |
| return false |
| } |
| return true |
| } |