mirror of
https://github.com/alibaba/higress.git
synced 2026-05-27 06:07:27 +08:00
Record the progress of the OSPP 2023 hgctl project (#453)
This commit is contained in:
744
pkg/cmd/hgctl/plugin/install/asker.go
Normal file
744
pkg/cmd/hgctl/plugin/install/asker.go
Normal file
@@ -0,0 +1,744 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed 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 install
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/plugin/types"
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/plugin/utils"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/santhosh-tekuri/jsonschema/v5"
|
||||
)
|
||||
|
||||
const (
|
||||
askInterrupted = "X Interrupted."
|
||||
invalidSyntax = "X Invalid syntax."
|
||||
failedToValidate = "X Failed to validate: not satisfied with schema."
|
||||
addConfSuccessful = "√ Successful to add configuration."
|
||||
)
|
||||
|
||||
var iconIdent = strings.Repeat(" ", 2)
|
||||
|
||||
type Asker interface {
|
||||
Ask() error
|
||||
}
|
||||
|
||||
type WasmPluginSpecConfAsker struct {
|
||||
resp *WasmPluginSpecConf
|
||||
|
||||
ingAsk *IngressAsker
|
||||
domAsk *DomainAsker
|
||||
glcAsk *GlobalConfAsker
|
||||
|
||||
printer *utils.YesOrNoPrinter
|
||||
}
|
||||
|
||||
func NewWasmPluginSpecConfAsker(ingAsk *IngressAsker, domAsk *DomainAsker, glcAsk *GlobalConfAsker, printer *utils.YesOrNoPrinter) *WasmPluginSpecConfAsker {
|
||||
return &WasmPluginSpecConfAsker{
|
||||
ingAsk: ingAsk,
|
||||
domAsk: domAsk,
|
||||
glcAsk: glcAsk,
|
||||
printer: printer,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *WasmPluginSpecConfAsker) Ask() error {
|
||||
var (
|
||||
wpc = NewPluginSpecConf()
|
||||
|
||||
globalConf map[string]interface{}
|
||||
ingressRule *IngressMatchRule
|
||||
domainRule *DomainMatchRule
|
||||
|
||||
scopeA = newScopeAsker(p.printer)
|
||||
rewriteA = newRewriteAsker(p.printer)
|
||||
ruleA = newRuleAsker(p.printer)
|
||||
|
||||
complete = false
|
||||
)
|
||||
|
||||
for {
|
||||
err := scopeA.Ask()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
scope := scopeA.resp
|
||||
|
||||
switch scope {
|
||||
case types.ScopeInstance:
|
||||
err = ruleA.Ask()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rule := ruleA.resp
|
||||
|
||||
switch rule {
|
||||
case ruleIngress:
|
||||
if ingressRule != nil {
|
||||
p.printer.Yesf("\n%s\n", ingressRule)
|
||||
err = rewriteA.Ask()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !rewriteA.resp {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
p.ingAsk.scope = scope
|
||||
err = p.ingAsk.Ask()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ingressRule = p.ingAsk.resp
|
||||
|
||||
case ruleDomain:
|
||||
if domainRule != nil {
|
||||
p.printer.Yesf("\n%s\n", domainRule)
|
||||
err = rewriteA.Ask()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !rewriteA.resp {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
p.domAsk.scope = scope
|
||||
err = p.domAsk.Ask()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
domainRule = p.domAsk.resp
|
||||
}
|
||||
|
||||
case types.ScopeGlobal:
|
||||
if globalConf != nil {
|
||||
b, _ := utils.MarshalYamlWithIndent(globalConf, 2)
|
||||
p.printer.Yesf("\n%s\n", string(b))
|
||||
err = rewriteA.Ask()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !rewriteA.resp {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
p.glcAsk.scope = scope
|
||||
err = p.glcAsk.Ask()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
globalConf = p.glcAsk.resp
|
||||
|
||||
case "Complete":
|
||||
complete = true
|
||||
break
|
||||
}
|
||||
|
||||
if complete {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if globalConf != nil {
|
||||
wpc.DefaultConfig = globalConf
|
||||
}
|
||||
if ingressRule != nil {
|
||||
wpc.MatchRules = append(wpc.MatchRules, ingressRule)
|
||||
}
|
||||
if domainRule != nil {
|
||||
wpc.MatchRules = append(wpc.MatchRules, domainRule)
|
||||
}
|
||||
|
||||
p.printer.Yesln("The complete configuration is as follows:")
|
||||
p.printer.Yesf("\n%s\n", wpc)
|
||||
p.resp = wpc
|
||||
return nil
|
||||
}
|
||||
|
||||
type IngressAsker struct {
|
||||
resp *IngressMatchRule
|
||||
|
||||
structName string
|
||||
schema *types.JSONSchemaProps
|
||||
scope types.Scope
|
||||
|
||||
vld *jsonschema.Schema // for validation
|
||||
printer *utils.YesOrNoPrinter
|
||||
}
|
||||
|
||||
func NewIngressAsker(structName string, schema *types.JSONSchemaProps, vld *jsonschema.Schema, printer *utils.YesOrNoPrinter) *IngressAsker {
|
||||
return &IngressAsker{
|
||||
structName: structName,
|
||||
schema: schema,
|
||||
vld: vld,
|
||||
printer: printer,
|
||||
}
|
||||
}
|
||||
|
||||
func (i *IngressAsker) Ask() error {
|
||||
continueA := newContinueAsker(i.printer)
|
||||
ings := make([]string, 0)
|
||||
for {
|
||||
var ing string
|
||||
err := utils.AskOne(&survey.Input{
|
||||
Message: "Enter the matched ingress:",
|
||||
Help: "Matching ingress resource object, the matching format is: namespace/ingress name",
|
||||
}, &ing)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ing = strings.TrimSpace(ing)
|
||||
if ing != "" {
|
||||
ings = append(ings, ing)
|
||||
}
|
||||
|
||||
err = continueA.Ask()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !continueA.resp {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
i.printer.Yesln(iconIdent + "Ingress:")
|
||||
as, err := recursivePrompt(i.structName, i.schema, i.scope, i.printer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if ok, ve := validate(i.vld, as); !ok {
|
||||
i.printer.Noln(failedToValidate)
|
||||
i.printer.Noln(ve)
|
||||
return nil
|
||||
}
|
||||
|
||||
i.resp = &IngressMatchRule{
|
||||
Ingress: ings,
|
||||
Config: as,
|
||||
}
|
||||
i.printer.Yesln(addConfSuccessful)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type DomainAsker struct {
|
||||
resp *DomainMatchRule
|
||||
|
||||
structName string
|
||||
schema *types.JSONSchemaProps
|
||||
scope types.Scope
|
||||
|
||||
vld *jsonschema.Schema // for validation
|
||||
printer *utils.YesOrNoPrinter
|
||||
}
|
||||
|
||||
func NewDomainAsker(structName string, schema *types.JSONSchemaProps, vld *jsonschema.Schema, printer *utils.YesOrNoPrinter) *DomainAsker {
|
||||
return &DomainAsker{
|
||||
structName: structName,
|
||||
schema: schema,
|
||||
vld: vld,
|
||||
printer: printer,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DomainAsker) Ask() error {
|
||||
continueA := newContinueAsker(d.printer)
|
||||
doms := make([]string, 0)
|
||||
for {
|
||||
var dom string
|
||||
err := utils.AskOne(&survey.Input{
|
||||
Message: "Enter the matched domain:",
|
||||
Help: "match domain name, support generic domain name",
|
||||
}, &dom)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dom = strings.TrimSpace(dom)
|
||||
if dom != "" {
|
||||
doms = append(doms, dom)
|
||||
}
|
||||
|
||||
err = continueA.Ask()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !continueA.resp {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
d.printer.Yesln(iconIdent + "Domain:")
|
||||
as, err := recursivePrompt(d.structName, d.schema, d.scope, d.printer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if ok, ve := validate(d.vld, as); !ok {
|
||||
d.printer.Noln(failedToValidate)
|
||||
d.printer.Noln(ve)
|
||||
return nil
|
||||
}
|
||||
|
||||
d.resp = &DomainMatchRule{
|
||||
Domain: doms,
|
||||
Config: as,
|
||||
}
|
||||
d.printer.Yesln(addConfSuccessful)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type GlobalConfAsker struct {
|
||||
resp map[string]interface{}
|
||||
|
||||
structName string
|
||||
schema *types.JSONSchemaProps
|
||||
scope types.Scope
|
||||
|
||||
vld *jsonschema.Schema // for validation
|
||||
printer *utils.YesOrNoPrinter
|
||||
}
|
||||
|
||||
func NewGlobalConfAsker(structName string, schema *types.JSONSchemaProps, vld *jsonschema.Schema, printer *utils.YesOrNoPrinter) *GlobalConfAsker {
|
||||
return &GlobalConfAsker{
|
||||
structName: structName,
|
||||
schema: schema,
|
||||
vld: vld,
|
||||
printer: printer,
|
||||
}
|
||||
}
|
||||
|
||||
func (g *GlobalConfAsker) Ask() error {
|
||||
g.printer.Yesln(iconIdent + "Global:")
|
||||
as, err := recursivePrompt(g.structName, g.schema, g.scope, g.printer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if ok, ve := validate(g.vld, as); !ok {
|
||||
g.printer.Noln(failedToValidate)
|
||||
g.printer.Noln(ve)
|
||||
return nil
|
||||
}
|
||||
|
||||
g.resp = as.(map[string]interface{})
|
||||
g.printer.Yesln(addConfSuccessful)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type continueAsker struct {
|
||||
resp bool
|
||||
|
||||
printer *utils.YesOrNoPrinter
|
||||
}
|
||||
|
||||
func newContinueAsker(printer *utils.YesOrNoPrinter) *continueAsker {
|
||||
return &continueAsker{printer: printer}
|
||||
}
|
||||
|
||||
func (c *continueAsker) Ask() error {
|
||||
resp := true
|
||||
err := utils.AskOne(&survey.Confirm{
|
||||
Message: fmt.Sprintf("%scontinue?", c.printer.Ident()),
|
||||
Default: true,
|
||||
}, &resp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.resp = resp
|
||||
return nil
|
||||
}
|
||||
|
||||
type rewriteAsker struct {
|
||||
resp bool
|
||||
|
||||
printer *utils.YesOrNoPrinter
|
||||
}
|
||||
|
||||
func newRewriteAsker(printer *utils.YesOrNoPrinter) *rewriteAsker {
|
||||
return &rewriteAsker{printer: printer}
|
||||
}
|
||||
|
||||
func (r *rewriteAsker) Ask() error {
|
||||
resp := false
|
||||
err := utils.AskOne(&survey.Confirm{
|
||||
Message: fmt.Sprintf("%sThe configuration already exists as shown above. Do you want to rewrite it?", r.printer.Ident()),
|
||||
Default: false,
|
||||
}, &resp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r.resp = resp
|
||||
return nil
|
||||
}
|
||||
|
||||
type scopeAsker struct {
|
||||
resp types.Scope
|
||||
|
||||
printer *utils.YesOrNoPrinter
|
||||
}
|
||||
|
||||
func newScopeAsker(printer *utils.YesOrNoPrinter) *scopeAsker {
|
||||
return &scopeAsker{printer: printer}
|
||||
}
|
||||
|
||||
func (s *scopeAsker) Ask() error {
|
||||
var resp string
|
||||
err := utils.AskOne(&survey.Select{
|
||||
Message: fmt.Sprintf("%sChoose a configuration effective scope or complete:", s.printer.Ident()),
|
||||
Options: []string{
|
||||
// TODO(WeixinX): Not visible to the user, instead Global, Ingress, and Domain are asked in ruleAsker
|
||||
string(types.ScopeInstance),
|
||||
string(types.ScopeGlobal),
|
||||
"Complete",
|
||||
},
|
||||
Default: string(types.ScopeInstance),
|
||||
}, &resp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.resp = types.Scope(resp)
|
||||
return nil
|
||||
}
|
||||
|
||||
type ruleAsker struct {
|
||||
resp Rule
|
||||
|
||||
printer *utils.YesOrNoPrinter
|
||||
}
|
||||
|
||||
func newRuleAsker(printer *utils.YesOrNoPrinter) *ruleAsker {
|
||||
return &ruleAsker{printer: printer}
|
||||
}
|
||||
|
||||
func (r *ruleAsker) Ask() error {
|
||||
var resp string
|
||||
err := utils.AskOne(&survey.Select{
|
||||
Message: fmt.Sprintf("%sChoose Ingress or Domain:", r.printer.Ident()),
|
||||
Options: []string{
|
||||
string(ruleIngress),
|
||||
string(ruleDomain),
|
||||
},
|
||||
Default: string(ruleIngress),
|
||||
}, &resp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r.resp = Rule(resp)
|
||||
return nil
|
||||
}
|
||||
|
||||
type WasmPluginSpecConf struct {
|
||||
DefaultConfig map[string]interface{} `yaml:"defaultConfig,omitempty"`
|
||||
MatchRules []MatchRule `yaml:"matchRules,omitempty"`
|
||||
}
|
||||
|
||||
func NewPluginSpecConf() *WasmPluginSpecConf {
|
||||
return &WasmPluginSpecConf{
|
||||
MatchRules: make([]MatchRule, 0),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *WasmPluginSpecConf) String() string {
|
||||
if len(p.DefaultConfig) == 0 && len(p.MatchRules) == 0 {
|
||||
return " "
|
||||
}
|
||||
|
||||
b, _ := utils.MarshalYamlWithIndent(p, 2)
|
||||
return string(b)
|
||||
}
|
||||
|
||||
type MatchRule interface {
|
||||
String() string
|
||||
}
|
||||
|
||||
type IngressMatchRule struct {
|
||||
Ingress []string `json:"ingress" yaml:"ingress" mapstructure:"ingress"`
|
||||
Config interface{} `json:"config" yaml:"config" mapstructure:"config"`
|
||||
}
|
||||
|
||||
func (i IngressMatchRule) String() string {
|
||||
b, _ := utils.MarshalYamlWithIndent(i, 2)
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func decodeIngressMatchRule(obj map[string]interface{}) (*IngressMatchRule, error) {
|
||||
var ing IngressMatchRule
|
||||
if err := mapstructure.Decode(obj, &ing); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &ing, nil
|
||||
}
|
||||
|
||||
type DomainMatchRule struct {
|
||||
Domain []string `json:"domain" yaml:"domain" mapstructure:"domain"`
|
||||
Config interface{} `json:"config" yaml:"config" mapstructure:"config"`
|
||||
}
|
||||
|
||||
func (d DomainMatchRule) String() string {
|
||||
b, _ := utils.MarshalYamlWithIndent(d, 2)
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func decodeDomainMatchRule(obj map[string]interface{}) (*DomainMatchRule, error) {
|
||||
var dom DomainMatchRule
|
||||
if err := mapstructure.Decode(obj, &dom); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &dom, nil
|
||||
}
|
||||
|
||||
type Rule string
|
||||
|
||||
const (
|
||||
ruleIngress Rule = "Ingress"
|
||||
ruleDomain Rule = "Domain"
|
||||
)
|
||||
|
||||
func recursivePrompt(structName string, schema *types.JSONSchemaProps, selScope types.Scope, printer *utils.YesOrNoPrinter) (interface{}, error) {
|
||||
printer.IncIdentRepeat()
|
||||
defer printer.DecIndentRepeat()
|
||||
return doPrompt(structName, nil, schema, types.ScopeAll, selScope, printer)
|
||||
}
|
||||
|
||||
func doPrompt(fieldName string, parent, schema *types.JSONSchemaProps, oriScope, selScope types.Scope, printer *utils.YesOrNoPrinter) (interface{}, error) {
|
||||
if schema.Title == "" {
|
||||
schema.Title = fieldName
|
||||
}
|
||||
if schema.Description == "" {
|
||||
schema.Description = fieldName
|
||||
}
|
||||
required := true
|
||||
if parent != nil {
|
||||
required = isRequired(fieldName, parent.Required)
|
||||
}
|
||||
msg, help := fieldTips(fieldName, parent, schema, required, printer)
|
||||
|
||||
switch types.JsonType(schema.Type) {
|
||||
case types.JsonTypeObject:
|
||||
printer.Println(iconIdent + msg)
|
||||
obj := make(map[string]interface{})
|
||||
m := schema.GetPropertiesOrderMap()
|
||||
for _, name := range m.Keys() {
|
||||
propI, _ := m.Get(name)
|
||||
prop := propI.(types.JSONSchemaProps)
|
||||
|
||||
if parent == nil { // keep topmost scope
|
||||
if prop.Scope == types.ScopeGlobal {
|
||||
oriScope = types.ScopeGlobal
|
||||
} else if prop.Scope == types.ScopeInstance || prop.Scope == "" {
|
||||
oriScope = types.ScopeInstance
|
||||
}
|
||||
}
|
||||
|
||||
if !matchesScope(oriScope, selScope, prop.Scope) {
|
||||
continue
|
||||
}
|
||||
|
||||
printer.IncIdentRepeat()
|
||||
v, err := doPrompt(name, schema, &prop, oriScope, selScope, printer)
|
||||
printer.DecIndentRepeat()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if v != nil {
|
||||
obj[name] = v
|
||||
}
|
||||
}
|
||||
|
||||
if len(obj) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return obj, nil
|
||||
|
||||
case types.JsonTypeArray:
|
||||
printer.Println(iconIdent + msg)
|
||||
continueA := newContinueAsker(printer)
|
||||
arr := make([]interface{}, 0)
|
||||
for {
|
||||
printer.IncIdentRepeat()
|
||||
v, err := doPrompt("item", schema, schema.Items.Schema, oriScope, selScope, printer)
|
||||
if err != nil {
|
||||
printer.DecIndentRepeat()
|
||||
return nil, err
|
||||
}
|
||||
if v != nil {
|
||||
arr = append(arr, v)
|
||||
}
|
||||
|
||||
err = continueA.Ask()
|
||||
printer.DecIndentRepeat()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !continueA.resp {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if len(arr) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return arr, nil
|
||||
|
||||
case types.JsonTypeInteger, types.JsonTypeNumber, types.JsonTypeBoolean, types.JsonTypeString:
|
||||
for {
|
||||
var inp string
|
||||
if err := utils.AskOne(&survey.Input{
|
||||
Message: msg,
|
||||
Help: help,
|
||||
}, &inp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if inp == "" && !required {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
switch types.JsonType(schema.Type) {
|
||||
case types.JsonTypeInteger:
|
||||
v, err := strconv.ParseInt(inp, 10, 64)
|
||||
if err != nil {
|
||||
if errors.Is(err, strconv.ErrSyntax) {
|
||||
printer.Nof("%s %q type is invalid.\n", invalidSyntax, inp)
|
||||
continue
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return v, nil
|
||||
case types.JsonTypeNumber:
|
||||
v, err := strconv.ParseFloat(inp, 64)
|
||||
if err != nil {
|
||||
if errors.Is(err, strconv.ErrSyntax) {
|
||||
printer.Nof("%s %q type is invalid.\n", invalidSyntax, inp)
|
||||
continue
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return v, nil
|
||||
case types.JsonTypeBoolean:
|
||||
v, err := strconv.ParseBool(inp)
|
||||
if err != nil {
|
||||
if errors.Is(err, strconv.ErrSyntax) {
|
||||
printer.Nof("%s %q type is invalid.\n", invalidSyntax, inp)
|
||||
continue
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return v, nil
|
||||
case types.JsonTypeString:
|
||||
return inp, nil
|
||||
default:
|
||||
return inp, nil
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported type: %s", schema.Type)
|
||||
}
|
||||
}
|
||||
|
||||
func matchesScope(oriScope, selScope, scope types.Scope) bool {
|
||||
return (oriScope == selScope) ||
|
||||
(selScope == types.ScopeInstance && (scope == selScope || scope == "" || scope == types.ScopeAll)) ||
|
||||
(selScope == types.ScopeGlobal && (scope == selScope || scope == types.ScopeAll))
|
||||
}
|
||||
|
||||
func fieldTips(fieldName string, parent, schema *types.JSONSchemaProps, required bool, printer *utils.YesOrNoPrinter) (string, string) {
|
||||
var msg, help string
|
||||
if fieldName == "item" {
|
||||
msg = fmt.Sprintf("%s%s(%s)", printer.Ident(), fieldName, schema.Type)
|
||||
help = fmt.Sprintf("%s%s: %s", printer.Ident(), parent.Title, parent.Description)
|
||||
} else {
|
||||
req := schema.JoinRequirementsBy(types.I18nEN_US, required)
|
||||
msg = fmt.Sprintf("%s%s(%s, %s)", printer.Ident(), fieldName, schema.Type, req)
|
||||
help = fmt.Sprintf("%s%s: %s", printer.Ident(), schema.Title, schema.Description)
|
||||
}
|
||||
|
||||
return msg, help
|
||||
}
|
||||
|
||||
func isRequired(name string, required []string) bool {
|
||||
req := false
|
||||
for _, n := range required {
|
||||
if name == n {
|
||||
req = true
|
||||
break
|
||||
}
|
||||
}
|
||||
return req
|
||||
}
|
||||
|
||||
func validate(schema *jsonschema.Schema, v interface{}) (bool, error) {
|
||||
if err := schema.Validate(v); err != nil {
|
||||
err = convertValidationError(err.(*jsonschema.ValidationError))
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func convertValidationError(ve *jsonschema.ValidationError) error {
|
||||
de := ve.DetailedOutput()
|
||||
if de.Valid {
|
||||
return nil
|
||||
}
|
||||
|
||||
errs := make([]error, 0)
|
||||
if de.Error != "" {
|
||||
errs = append(errs, errors.New(de.Error))
|
||||
}
|
||||
errs = append(errs, doConvertValidationError(de.Errors, errs)...)
|
||||
if len(errs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var ret error
|
||||
for i, err := range errs {
|
||||
if i == 0 {
|
||||
ret = fmt.Errorf("%w", err)
|
||||
} else {
|
||||
ret = fmt.Errorf("%s\n%w", ret.Error(), err)
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func doConvertValidationError(de []jsonschema.Detailed, errs []error) []error {
|
||||
for _, e := range de {
|
||||
if e.Error != "" {
|
||||
errs = append(errs, errors.New(e.Error))
|
||||
}
|
||||
if len(e.Errors) > 0 {
|
||||
errs = append(errs, doConvertValidationError(e.Errors, errs)...)
|
||||
}
|
||||
}
|
||||
return errs
|
||||
}
|
||||
383
pkg/cmd/hgctl/plugin/install/install.go
Normal file
383
pkg/cmd/hgctl/plugin/install/install.go
Normal file
@@ -0,0 +1,383 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed 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 install
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
k8s "github.com/alibaba/higress/pkg/cmd/hgctl/kubernetes"
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/plugin/build"
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/plugin/config"
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/plugin/option"
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/plugin/types"
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/plugin/utils"
|
||||
"github.com/alibaba/higress/pkg/cmd/options"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2/terminal"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/santhosh-tekuri/jsonschema/v5"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
"github.com/spf13/viper"
|
||||
k8serr "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
k8syaml "k8s.io/apimachinery/pkg/util/yaml"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
)
|
||||
|
||||
type installer struct {
|
||||
optionFile string
|
||||
bldOpts option.BuildOptions
|
||||
insOpts option.InstallOptions
|
||||
|
||||
cli *k8s.WasmPluginClient
|
||||
w io.Writer
|
||||
utils.Debugger
|
||||
}
|
||||
|
||||
func NewCommand() *cobra.Command {
|
||||
var ins installer
|
||||
v := viper.New()
|
||||
|
||||
installCmd := &cobra.Command{
|
||||
Use: "install",
|
||||
Aliases: []string{"ins", "i"},
|
||||
Short: "Install WASM plugin",
|
||||
Example: ` # Install WASM plugin using a WasmPlugin manifest
|
||||
hgctl plugin install -y plugin-conf.yaml
|
||||
|
||||
# Install WASM plugin through the Golang WASM plugin project (do it by relying on option.yaml now)
|
||||
docker login
|
||||
hgctl plugin install -g ./
|
||||
`,
|
||||
|
||||
PreRun: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.CheckErr(ins.config(v, cmd))
|
||||
},
|
||||
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.CheckErr(ins.install(cmd.PersistentFlags()))
|
||||
},
|
||||
}
|
||||
|
||||
flags := installCmd.PersistentFlags()
|
||||
options.AddKubeConfigFlags(flags)
|
||||
option.AddOptionFileFlag(&ins.optionFile, flags)
|
||||
v.BindPFlags(flags)
|
||||
|
||||
flags.StringP("namespace", "n", k8s.HigressNamespace, "Namespace where Higress was installed")
|
||||
v.BindPFlag("install.namespace", flags.Lookup("namespace"))
|
||||
v.SetDefault("install.namespace", k8s.DefaultHigressNamespace)
|
||||
|
||||
flags.StringP("spec-yaml", "s", "./out/spec.yaml", "Use to validate WASM plugin configuration")
|
||||
v.BindPFlag("install.spec-yaml", flags.Lookup("spec-yaml"))
|
||||
v.SetDefault("install.spec-yaml", "./test/plugin-spec-yaml")
|
||||
|
||||
// TODO(WeixinX):
|
||||
// - Change "--from-yaml (-y)" to "--from-oci (-o)" and implement command line interaction like "--from-go-src"
|
||||
// - Add "--from-jar (-j)"
|
||||
flags.StringP("from-yaml", "y", "./test/plugin-conf.yaml", "Install WASM plugin using a WasmPlugin manifest")
|
||||
v.BindPFlag("install.from-yaml", flags.Lookup("from-yaml"))
|
||||
v.SetDefault("install.from-yaml", "./test/plugin-conf.yaml")
|
||||
|
||||
flags.StringP("from-go-src", "g", "", "Install WASM plugin through the Golang WASM plugin project")
|
||||
v.BindPFlag("install.from-go-src", flags.Lookup("from-go-src"))
|
||||
v.SetDefault("install.from-go-src", "")
|
||||
|
||||
flags.BoolP("debug", "", false, "Enable debug mode")
|
||||
v.BindPFlag("install.debug", flags.Lookup("debug"))
|
||||
v.SetDefault("install.debug", false)
|
||||
|
||||
return installCmd
|
||||
}
|
||||
|
||||
func (ins *installer) config(v *viper.Viper, cmd *cobra.Command) error {
|
||||
allOpt, err := option.ParseOptions(ins.optionFile, v, cmd.PersistentFlags())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// TODO(WeixinX): Avoid relying on build options, add a new option "--push/--image" for installing from go src
|
||||
ins.bldOpts = allOpt.Build
|
||||
ins.insOpts = allOpt.Install
|
||||
|
||||
dynCli, err := k8s.NewDynamicClient(options.DefaultConfigFlags.ToRawKubeConfigLoader())
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to build kubernetes dynamic client")
|
||||
}
|
||||
ins.cli = k8s.NewWasmPluginClient(dynCli)
|
||||
ins.w = cmd.OutOrStdout()
|
||||
ins.Debugger = utils.NewDefaultDebugger(ins.insOpts.Debug, ins.w)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ins *installer) install(flags *pflag.FlagSet) (err error) {
|
||||
ins.Debugf("install option:\n%s\n", ins.String())
|
||||
|
||||
if ins.insOpts.FromGoSrc == "" || flags.Changed("from-yaml") {
|
||||
err = ins.yamlHandler()
|
||||
} else {
|
||||
err = ins.goHandler()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (ins *installer) yamlHandler() error {
|
||||
return ins.doInstall(true)
|
||||
}
|
||||
|
||||
func (ins *installer) goHandler() error {
|
||||
// 0. ensure output.type == image
|
||||
if ins.bldOpts.Output.Type != "image" {
|
||||
return errors.New("output type must be image")
|
||||
}
|
||||
|
||||
// 1. build the WASM plugin project and push the image to the registry
|
||||
bld, err := build.NewBuilder(func(b *build.Builder) error {
|
||||
b.BuildOptions = ins.bldOpts
|
||||
b.Debug = ins.insOpts.Debug
|
||||
b.WithManualClean() // keep spec.yaml
|
||||
b.WithWriter(ins.w)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to initialize builder")
|
||||
}
|
||||
err = bld.Build()
|
||||
if err != nil {
|
||||
bld.Debugln("clean up for error ...")
|
||||
bld.CleanupForError()
|
||||
return errors.Wrap(err, "failed to build and push wasm plugin")
|
||||
}
|
||||
defer bld.Cleanup()
|
||||
|
||||
// 2. command-line interaction lets the user enter the wasm plugin configuration
|
||||
specPath := bld.SpecYAMLPath()
|
||||
spec, err := types.ParseSpecYAML(specPath)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to parse spec.yaml: %s", specPath)
|
||||
}
|
||||
vld, err := buildSchemaValidator(spec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
example := spec.GetConfigExample()
|
||||
schema := spec.Spec.ConfigSchema.OpenAPIV3Schema
|
||||
printer := utils.DefaultPrinter()
|
||||
asker := NewWasmPluginSpecConfAsker(
|
||||
NewIngressAsker(bld.Model, schema, vld, printer),
|
||||
NewDomainAsker(bld.Model, schema, vld, printer),
|
||||
NewGlobalConfAsker(bld.Model, schema, vld, printer),
|
||||
printer,
|
||||
)
|
||||
|
||||
printer.Yesln("Please enter the configurations for the WASM plugin you want to install:")
|
||||
printer.Yesln("Configuration example:")
|
||||
printer.Yesf("\n%s\n", example)
|
||||
|
||||
err = asker.Ask()
|
||||
if err != nil {
|
||||
if errors.Is(err, terminal.InterruptErr) {
|
||||
printer.Noln(askInterrupted)
|
||||
return nil
|
||||
}
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// 3. generate the WasmPlugin manifest
|
||||
wpc := asker.resp
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to marshal wasm plugin config")
|
||||
}
|
||||
// get the parameters of plugin-conf.yaml from spec.yaml
|
||||
pc, err := config.ExtractPluginConfFrom(spec, wpc.String(), bld.Output.Dest)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to get the parameters of plugin-conf.yaml from %s", specPath)
|
||||
}
|
||||
ins.Debugf("plugin-conf.yaml params:\n%s\n", pc.String())
|
||||
if err = config.GenPluginConfYAML(pc, bld.TempDir()); err != nil {
|
||||
return errors.Wrap(err, "failed to generate plugin-conf.yaml")
|
||||
}
|
||||
|
||||
// 4. install by the manifest
|
||||
ins.insOpts.FromYaml = bld.TempDir() + "/plugin-conf.yaml"
|
||||
if err = ins.doInstall(false); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ins *installer) doInstall(validate bool) error {
|
||||
f, err := os.Open(ins.insOpts.FromYaml)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
// multiple WASM plugins are separated by '---' in yaml, but we only handle first one
|
||||
// TODO(WeixinX): Use WasmPlugin Object type instead of Unstructured
|
||||
obj := &unstructured.Unstructured{}
|
||||
dc := k8syaml.NewYAMLOrJSONDecoder(f, 4096)
|
||||
if err = dc.Decode(obj); err != nil {
|
||||
return errors.Wrapf(err, "failed to parse wasm plugin from manifest %q", ins.insOpts.FromYaml)
|
||||
}
|
||||
|
||||
if !isValidAPIVersion(obj) {
|
||||
fmt.Fprintf(ins.w, "Warning: wasm plugin %q has invalid apiVersion, automatically modified: %q -> %q\n",
|
||||
obj.GetName(), obj.GetAPIVersion(), k8s.HigressExtAPIVersion)
|
||||
obj.SetAPIVersion(k8s.HigressExtAPIVersion)
|
||||
}
|
||||
if !isValidKind(obj) {
|
||||
fmt.Fprintf(ins.w, "Warning: wasm plugin %q has invalid kind, automatically modified: %q -> %q\n",
|
||||
obj.GetName(), obj.GetKind(), k8s.WasmPluginKind)
|
||||
obj.SetKind(k8s.WasmPluginKind)
|
||||
}
|
||||
if !isValidNamespace(obj) {
|
||||
fmt.Fprintf(ins.w, "Warning: wasm plugin %q has invalid namespace, automatically modified: %q -> %q\n",
|
||||
obj.GetName(), obj.GetNamespace(), k8s.HigressNamespace)
|
||||
obj.SetNamespace(k8s.HigressNamespace)
|
||||
}
|
||||
|
||||
// validate wasm plugin config
|
||||
if validate {
|
||||
if wps, ok := obj.Object["spec"].(map[string]interface{}); ok {
|
||||
if err = ins.validateWasmPluginConfig(wps); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
return errors.New("failed to get the spec filed of wasm plugin")
|
||||
}
|
||||
ins.Debugln("successfully validated wasm plugin config")
|
||||
}
|
||||
|
||||
result, err := ins.cli.Create(context.TODO(), obj)
|
||||
if err != nil {
|
||||
if k8serr.IsAlreadyExists(err) {
|
||||
fmt.Fprintf(ins.w, "wasm plugin %q already exists\n",
|
||||
fmt.Sprintf("%s/%s", obj.GetNamespace(), obj.GetName()))
|
||||
return nil
|
||||
}
|
||||
return errors.Wrapf(err, "failed to install wasm plugin %q",
|
||||
fmt.Sprintf("%s/%s", obj.GetNamespace(), obj.GetName()))
|
||||
}
|
||||
|
||||
fmt.Fprintf(ins.w, "Installed wasm plugin %q\n", fmt.Sprintf("%s/%s", result.GetNamespace(), result.GetName()))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func isValidAPIVersion(obj *unstructured.Unstructured) bool {
|
||||
return obj.GetAPIVersion() == k8s.HigressExtAPIVersion
|
||||
}
|
||||
|
||||
func isValidKind(obj *unstructured.Unstructured) bool {
|
||||
return obj.GetKind() == k8s.WasmPluginKind
|
||||
}
|
||||
|
||||
func isValidNamespace(obj *unstructured.Unstructured) bool {
|
||||
return obj.GetNamespace() == k8s.HigressNamespace
|
||||
}
|
||||
|
||||
func (ins *installer) validateWasmPluginConfig(wps map[string]interface{}) error {
|
||||
spec, err := types.ParseSpecYAML(ins.insOpts.SpecYaml)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to parse %s", ins.insOpts.SpecYaml)
|
||||
}
|
||||
vld, err := buildSchemaValidator(spec)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to build schema validator")
|
||||
}
|
||||
|
||||
if dc, ok := wps["defaultConfig"].(map[string]interface{}); ok {
|
||||
if ok, err = validate(vld, dc); !ok {
|
||||
return errors.Wrap(err, "failed to validate default config")
|
||||
}
|
||||
|
||||
// debug
|
||||
b, _ := utils.MarshalYamlWithIndent(dc, 2)
|
||||
ins.Debugf("default config:\n%s\n", string(b))
|
||||
}
|
||||
|
||||
if mrs, ok := wps["matchRules"].([]interface{}); ok {
|
||||
for _, mr := range mrs {
|
||||
if r, ok := mr.(map[string]interface{}); ok {
|
||||
if _, ok = r["ingress"]; ok {
|
||||
ing, err := decodeIngressMatchRule(r)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to parse ingress match rule")
|
||||
}
|
||||
if ok, err = validate(vld, ing.Config); !ok {
|
||||
return errors.Wrap(err, "failed to validate ingress match rule")
|
||||
}
|
||||
|
||||
ins.Debugf("ingress match rule:\n%s\n", ing.String())
|
||||
|
||||
} else if _, ok = r["domain"]; ok {
|
||||
dom, err := decodeDomainMatchRule(r)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to parse domain match rule")
|
||||
}
|
||||
if ok, err = validate(vld, dom.Config); !ok {
|
||||
return errors.Wrap(err, "failed to validate ingress match rule")
|
||||
}
|
||||
|
||||
ins.Debugf("domain match rule:\n%s\n", dom.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildSchemaValidator(spec *types.WasmPluginMeta) (*jsonschema.Schema, error) {
|
||||
if spec == nil {
|
||||
return nil, errors.New("spec is nil")
|
||||
}
|
||||
|
||||
schema := spec.Spec.ConfigSchema.OpenAPIV3Schema
|
||||
if schema == nil {
|
||||
return nil, errors.New("spec has no config schema")
|
||||
}
|
||||
|
||||
b, err := json.Marshal(schema)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c := jsonschema.NewCompiler()
|
||||
c.Draft = jsonschema.Draft4
|
||||
err = c.AddResource("schema.json", strings.NewReader(string(b)))
|
||||
vld, err := c.Compile("schema.json")
|
||||
if err != nil {
|
||||
errors.Wrap(err, "failed to compile schema")
|
||||
}
|
||||
|
||||
return vld, nil
|
||||
}
|
||||
|
||||
func (ins *installer) String() string {
|
||||
b, err := json.MarshalIndent(ins.insOpts, "", " ")
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("OptionFile: %s\n%s", ins.optionFile, string(b))
|
||||
}
|
||||
Reference in New Issue
Block a user