mirror of
https://github.com/alibaba/higress.git
synced 2026-06-10 05:07:30 +08:00
Record the progress of the OSPP 2023 hgctl project (#453)
This commit is contained in:
111
pkg/cmd/hgctl/docker/compose.go
Normal file
111
pkg/cmd/hgctl/docker/compose.go
Normal file
@@ -0,0 +1,111 @@
|
||||
// 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 docker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/compose-spec/compose-go/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/flags"
|
||||
"github.com/docker/compose/v2/cmd/formatter"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v2/pkg/compose"
|
||||
)
|
||||
|
||||
type Compose struct {
|
||||
client *api.ServiceProxy
|
||||
w io.Writer
|
||||
}
|
||||
|
||||
func NewCompose(w io.Writer) (*Compose, error) {
|
||||
c := &Compose{w: w}
|
||||
|
||||
dockerCli, err := command.NewDockerCli(
|
||||
command.WithCombinedStreams(c.w),
|
||||
// command.WithDefaultContextStoreConfig(), Deprecated, set during NewDockerCli
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
opts := flags.NewClientOptions()
|
||||
err = dockerCli.Initialize(opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.client = api.NewServiceProxy().WithService(compose.NewComposeService(dockerCli.Client(), dockerCli.ConfigFile()))
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (c Compose) Up(ctx context.Context, name string, configs []string, source string, detach bool) error {
|
||||
pOpts, err := cli.NewProjectOptions(
|
||||
configs,
|
||||
cli.WithWorkingDirectory(source),
|
||||
cli.WithDefaultConfigPath,
|
||||
cli.WithName(name),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
project, err := cli.ProjectFromOptions(pOpts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i, s := range project.Services {
|
||||
// TODO(WeixinX): Change from `Label` to `CustomLabels` after upgrading the dependency library github.com/compose-spec/compose-go
|
||||
s.Labels = map[string]string{
|
||||
api.ProjectLabel: project.Name,
|
||||
api.ServiceLabel: s.Name,
|
||||
api.VersionLabel: api.ComposeVersion,
|
||||
api.WorkingDirLabel: project.WorkingDir,
|
||||
api.ConfigFilesLabel: strings.Join(project.ComposeFiles, ","),
|
||||
api.OneoffLabel: "False",
|
||||
}
|
||||
project.Services[i] = s
|
||||
}
|
||||
project.WithoutUnnecessaryResources()
|
||||
|
||||
// for log
|
||||
var consumer api.LogConsumer
|
||||
if !detach {
|
||||
// TODO(WeixinX): Change to `formatter.NewLogConsumer(ctx, c.w, c.w, true, true, false)` after upgrading the dependency library github.com/compose-spec/compose-go
|
||||
consumer = formatter.NewLogConsumer(ctx, c.w, true, true)
|
||||
}
|
||||
attachTo := make([]string, 0)
|
||||
for _, svc := range project.Services {
|
||||
attachTo = append(attachTo, svc.Name)
|
||||
}
|
||||
|
||||
return c.client.Up(ctx, project, api.UpOptions{
|
||||
Start: api.StartOptions{
|
||||
Attach: consumer,
|
||||
AttachTo: attachTo,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (c Compose) List(ctx context.Context) ([]api.Stack, error) {
|
||||
return c.client.List(ctx, api.ListOptions{})
|
||||
}
|
||||
|
||||
func (c Compose) Down(ctx context.Context, name string) error {
|
||||
return c.client.Down(ctx, name, api.DownOptions{})
|
||||
}
|
||||
130
pkg/cmd/hgctl/kubernetes/wasmplugin.go
Normal file
130
pkg/cmd/hgctl/kubernetes/wasmplugin.go
Normal file
@@ -0,0 +1,130 @@
|
||||
// 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 kubernetes
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/client-go/dynamic"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultHigressNamespace = "higress-system"
|
||||
HigressExtGroup = "extensions.higress.io"
|
||||
HigressExtVersion = "v1alpha1"
|
||||
HigressExtAPIVersion = HigressExtGroup + "/" + HigressExtVersion
|
||||
|
||||
WasmPluginKind = "WasmPlugin"
|
||||
WasmPluginResource = "wasmplugins"
|
||||
)
|
||||
|
||||
var (
|
||||
HigressNamespace = DefaultHigressNamespace
|
||||
WasmPluginGVK = schema.GroupVersionKind{Group: HigressExtGroup, Version: HigressExtVersion, Kind: WasmPluginKind}
|
||||
WasmPluginGVR = schema.GroupVersionResource{Group: HigressExtGroup, Version: HigressExtVersion, Resource: WasmPluginResource}
|
||||
)
|
||||
|
||||
func AddHigressNamespaceFlags(flags *pflag.FlagSet) {
|
||||
flags.StringVarP(&HigressNamespace, "namespace", "n",
|
||||
DefaultHigressNamespace, "Namespace where Higress was installed")
|
||||
}
|
||||
|
||||
type WasmPluginClient struct {
|
||||
dyn *DynamicClient
|
||||
}
|
||||
|
||||
func NewWasmPluginClient(dynClient *DynamicClient) *WasmPluginClient {
|
||||
return &WasmPluginClient{dynClient}
|
||||
}
|
||||
|
||||
func (c WasmPluginClient) Get(ctx context.Context, name string) (*unstructured.Unstructured, error) {
|
||||
return c.dyn.Get(ctx, WasmPluginGVR, HigressNamespace, name)
|
||||
}
|
||||
|
||||
func (c WasmPluginClient) List(ctx context.Context) (*unstructured.UnstructuredList, error) {
|
||||
return c.dyn.List(ctx, WasmPluginGVR, HigressNamespace)
|
||||
}
|
||||
|
||||
func (c WasmPluginClient) Create(ctx context.Context, obj *unstructured.Unstructured) (*unstructured.Unstructured, error) {
|
||||
return c.dyn.Create(ctx, WasmPluginGVR, HigressNamespace, obj)
|
||||
}
|
||||
|
||||
func (c WasmPluginClient) Delete(ctx context.Context, name string) (*unstructured.Unstructured, error) {
|
||||
return c.dyn.Delete(ctx, WasmPluginGVR, HigressNamespace, name)
|
||||
}
|
||||
|
||||
func (c WasmPluginClient) Update(ctx context.Context, obj *unstructured.Unstructured) (*unstructured.Unstructured, error) {
|
||||
return c.dyn.Update(ctx, WasmPluginGVR, HigressNamespace, obj)
|
||||
}
|
||||
|
||||
// TODO(WeixinX): Will be changed to WasmPlugin specific Client instead of Unstructured
|
||||
type DynamicClient struct {
|
||||
config *rest.Config
|
||||
client dynamic.Interface
|
||||
}
|
||||
|
||||
func NewDynamicClient(clientConfig clientcmd.ClientConfig) (*DynamicClient, error) {
|
||||
var (
|
||||
c DynamicClient
|
||||
err error
|
||||
)
|
||||
|
||||
c.config, err = clientConfig.ClientConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.client, err = dynamic.NewForConfig(c.config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &c, nil
|
||||
}
|
||||
|
||||
func (c DynamicClient) Get(ctx context.Context, gvr schema.GroupVersionResource, namespace, name string) (*unstructured.Unstructured, error) {
|
||||
return c.client.Resource(gvr).Namespace(namespace).Get(ctx, name, metav1.GetOptions{})
|
||||
}
|
||||
|
||||
func (c DynamicClient) List(ctx context.Context, gvr schema.GroupVersionResource, namespace string) (*unstructured.UnstructuredList, error) {
|
||||
return c.client.Resource(gvr).Namespace(namespace).List(ctx, metav1.ListOptions{})
|
||||
}
|
||||
|
||||
func (c DynamicClient) Create(ctx context.Context, gvr schema.GroupVersionResource, namespace string, obj *unstructured.Unstructured) (*unstructured.Unstructured, error) {
|
||||
return c.client.Resource(gvr).Namespace(namespace).Create(ctx, obj, metav1.CreateOptions{})
|
||||
}
|
||||
|
||||
func (c DynamicClient) Delete(ctx context.Context, gvr schema.GroupVersionResource, namespace, name string) (*unstructured.Unstructured, error) {
|
||||
result, err := c.client.Resource(gvr).Namespace(namespace).Get(ctx, name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = c.client.Resource(gvr).Namespace(namespace).Delete(ctx, name, metav1.DeleteOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (c DynamicClient) Update(ctx context.Context, gvr schema.GroupVersionResource, namespace string,
|
||||
obj *unstructured.Unstructured) (*unstructured.Unstructured, error) {
|
||||
return c.client.Resource(gvr).Namespace(namespace).Update(ctx, obj, metav1.UpdateOptions{})
|
||||
}
|
||||
778
pkg/cmd/hgctl/plugin/build/build.go
Normal file
778
pkg/cmd/hgctl/plugin/build/build.go
Normal file
@@ -0,0 +1,778 @@
|
||||
// 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 build
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/signal"
|
||||
"os/user"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/plugin/option"
|
||||
ptypes "github.com/alibaba/higress/pkg/cmd/hgctl/plugin/types"
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/plugin/utils"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/mount"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/docker/docker/pkg/stdcopy"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
"github.com/spf13/viper"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultBuilderRepository = "higress-registry.cn-hangzhou.cr.aliyuncs.com/plugins/wasm-go-builder"
|
||||
DefaultBuilderGo = "1.19"
|
||||
DefaultBuilderTinyGo = "0.28.1"
|
||||
DefaultBuilderOras = "1.0.0"
|
||||
|
||||
MediaTypeSpec = "application/vnd.module.wasm.spec.v1+yaml"
|
||||
MediaTypeREADME = "application/vnd.module.wasm.doc.v1+markdown"
|
||||
MediaTypeREADME_ZH = "application/vnd.module.wasm.doc.v1.zh+markdown"
|
||||
MediaTypeREADME_EN = "application/vnd.module.wasm.doc.v1.en+markdown"
|
||||
MediaTypeIcon = "application/vnd.module.wasm.icon.v1+png"
|
||||
MediaTypePlugin = "application/vnd.oci.image.layer.v1.tar+gzip"
|
||||
|
||||
HostTempDirPattern = "higress-wasm-go-build-*"
|
||||
HostDockerEntryPattern = "higress-wasm-go-build-docker-entrypoint-*.sh"
|
||||
|
||||
ContainerWorkDir = "/workspace"
|
||||
ContainerTempDir = "/higress_temp" // the directory to temporarily store the build products
|
||||
ContainerOutDir = "/output"
|
||||
ContainerDockerAuth = "/root/.docker/config.json"
|
||||
ContainerEntryFile = "docker-entrypoint.sh"
|
||||
ContainerEntryFilePath = "/" + ContainerEntryFile
|
||||
)
|
||||
|
||||
type Builder struct {
|
||||
OptionFile string
|
||||
option.BuildOptions
|
||||
Username, Password string
|
||||
|
||||
repository string
|
||||
tempDir string
|
||||
dockerEntrypoint string
|
||||
uid, gid string
|
||||
manualClean bool
|
||||
|
||||
containerID string
|
||||
containerConf types.ContainerCreateConfig
|
||||
dockerCli *client.Client
|
||||
w io.Writer
|
||||
sig chan os.Signal // watch interrupt
|
||||
stop chan struct{} // stop the build process when an interruption occurs
|
||||
done chan struct{} // signal that the build process is finished
|
||||
|
||||
utils.Debugger
|
||||
*utils.YesOrNoPrinter
|
||||
}
|
||||
|
||||
func NewBuilder(f ConfigFunc) (*Builder, error) {
|
||||
b := new(Builder)
|
||||
if err := b.config(f); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func NewCommand() *cobra.Command {
|
||||
var bld Builder
|
||||
v := viper.New()
|
||||
|
||||
buildCmd := &cobra.Command{
|
||||
Use: "build",
|
||||
Aliases: []string{"bld", "b"},
|
||||
Short: "Build Golang WASM plugin",
|
||||
Example: ` # If the option.yaml file exists in the current path, do the following:
|
||||
hgctl plugin build
|
||||
|
||||
# Using "--model(-s)" to specify the WASM plugin configuration structure name, e.g. "HelloWorldConfig"
|
||||
hgctl plugin build --model HelloWorldConfig
|
||||
|
||||
# Using "--output-type(-t)" and "--output-dest(-d)" to push the build products as an OCI image to the specified repository
|
||||
docker login
|
||||
hgctl plugin build -s BasicAuthConfig -t image -d docker.io/<your_username>/<your_image>
|
||||
`,
|
||||
PreRun: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.CheckErr(bld.config(func(b *Builder) error {
|
||||
return b.parseOptions(v, cmd)
|
||||
}))
|
||||
},
|
||||
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.CheckErr(bld.Build())
|
||||
},
|
||||
}
|
||||
|
||||
bld.bindFlags(v, buildCmd.PersistentFlags())
|
||||
|
||||
return buildCmd
|
||||
}
|
||||
|
||||
func (b *Builder) bindFlags(v *viper.Viper, flags *pflag.FlagSet) {
|
||||
option.AddOptionFileFlag(&b.OptionFile, flags)
|
||||
flags.StringVarP(&b.Username, "username", "u", "", "Username for pushing image to the docker repository")
|
||||
flags.StringVarP(&b.Password, "password", "p", "", "Password for pushing image to the docker repository")
|
||||
v.BindPFlags(flags)
|
||||
|
||||
// this binding ensures that flags explicitly set on the command line have the
|
||||
// highest priority, and if they are not set, they are read from the configuration file.
|
||||
flags.StringP("builder-go", "g", DefaultBuilderGo, "Golang version in the official builder image")
|
||||
v.BindPFlag("build.builder.go", flags.Lookup("builder-go"))
|
||||
v.SetDefault("build.builder.go", DefaultBuilderGo)
|
||||
|
||||
flags.StringP("builder-tinygo", "n", DefaultBuilderTinyGo, "TinyGo version in the official builder image")
|
||||
v.BindPFlag("build.builder.tinygo", flags.Lookup("builder-tinygo"))
|
||||
v.SetDefault("build.builder.tinygo", DefaultBuilderTinyGo)
|
||||
|
||||
flags.StringP("builder-oras", "r", DefaultBuilderOras, "ORAS version in official the builder image")
|
||||
v.BindPFlag("build.builder.oras", flags.Lookup("builder-oras"))
|
||||
v.SetDefault("build.builder.oras", DefaultBuilderOras)
|
||||
|
||||
flags.StringP("input", "i", "./", "Directory of the WASM plugin project to be built")
|
||||
v.BindPFlag("build.input", flags.Lookup("input"))
|
||||
v.SetDefault("build.input", "./")
|
||||
|
||||
flags.StringP("output-type", "t", "files", "Output type of the build products. [files, image]")
|
||||
v.BindPFlag("build.output.type", flags.Lookup("output-type"))
|
||||
v.SetDefault("build.output.type", "files")
|
||||
|
||||
flags.StringP("output-dest", "d", "./out", "Output destination of the build products")
|
||||
v.BindPFlag("build.output.dest", flags.Lookup("output-dest"))
|
||||
v.SetDefault("build.output.dest", "./out")
|
||||
|
||||
flags.StringP("docker-auth", "a", "~/.docker/config.json", "Authentication configuration for pushing image to the docker repository")
|
||||
v.BindPFlag("build.docker-auth", flags.Lookup("docker-auth"))
|
||||
v.SetDefault("build.docker-auth", "~/.docker/config.json")
|
||||
|
||||
flags.StringP("model-dir", "m", "./", "Directory of the WASM plugin configuration structure")
|
||||
v.BindPFlag("build.model-dir", flags.Lookup("model-dir"))
|
||||
v.SetDefault("build.model-dir", "./")
|
||||
|
||||
flags.StringP("model", "s", "", "Structure name of the WASM plugin configuration")
|
||||
v.BindPFlag("build.model", flags.Lookup("model"))
|
||||
v.SetDefault("build.model", "PluginConfig")
|
||||
|
||||
flags.BoolP("debug", "", false, "Enable debug mode")
|
||||
v.BindPFlag("build.debug", flags.Lookup("debug"))
|
||||
v.SetDefault("build.debug", false)
|
||||
}
|
||||
|
||||
func (b *Builder) Build() (err error) {
|
||||
b.Debugf("build options: \n%s\n", b.String())
|
||||
|
||||
go func() {
|
||||
err = b.doBuild()
|
||||
}()
|
||||
|
||||
// wait for an interruption to occur or finishing the build
|
||||
select {
|
||||
case <-b.sig:
|
||||
b.interrupt()
|
||||
b.Nof("\nInterrupt ...\n")
|
||||
// wait for the doBuild process to exit, otherwise there will be unexpected bugs
|
||||
b.waitForFinished()
|
||||
// if the build process is interrupted, then we ignore the flag `manualClean` and clean up
|
||||
// TODO(WeixinX): How do we clean up uploaded image when an interruption occurs?
|
||||
b.Debugln("clean up for interrupting ...")
|
||||
b.CleanupForError()
|
||||
os.Exit(0)
|
||||
|
||||
case <-b.done:
|
||||
if err != nil {
|
||||
if !b.manualClean {
|
||||
b.Debugln("clean up for error ...")
|
||||
b.CleanupForError()
|
||||
}
|
||||
return
|
||||
}
|
||||
if !b.manualClean {
|
||||
b.Debugln("clean up for normal ...")
|
||||
b.Cleanup()
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
waitIcon = "[-]"
|
||||
successfulIcon = "[√]"
|
||||
)
|
||||
|
||||
func (b *Builder) doBuild() (err error) {
|
||||
// finish here does not mean that the build was successful,
|
||||
// but that the doBuild process is complete
|
||||
defer b.finish()
|
||||
|
||||
if err = b.generateMetadata(); err != nil {
|
||||
return errors.Wrap(err, "failed to generate wasm plugin metadata files")
|
||||
}
|
||||
|
||||
b.Printf("%s pull the builder image ...\n", waitIcon)
|
||||
ctx := context.TODO()
|
||||
if err = b.imagePull(ctx); err != nil {
|
||||
return errors.Wrapf(err, "failed to pull the builder image %s", b.builderImageRef())
|
||||
}
|
||||
b.Yesf("%s pull the builder image: %s\n", successfulIcon, b.builderImageRef())
|
||||
|
||||
if err = b.addContainerConfByOutType(); err != nil {
|
||||
return errors.Wrapf(err, "failed to add the additional container configuration for output type %q", b.Output.Type)
|
||||
}
|
||||
|
||||
b.Printf("%s create the builder container ...\n", waitIcon)
|
||||
if err = b.containerCreate(ctx); err != nil {
|
||||
return errors.Wrap(err, "failed to create the builder container")
|
||||
}
|
||||
b.Yesf("%s create the builder container: %s\n", successfulIcon, b.containerID)
|
||||
|
||||
b.Printf("%s start the builder container ...\n", waitIcon)
|
||||
if err = b.containerStart(ctx); err != nil {
|
||||
return errors.Wrap(err, "failed to start the builder container")
|
||||
}
|
||||
|
||||
if b.Output.Type == "files" {
|
||||
b.Yesf("%s finish building!\n", successfulIcon)
|
||||
} else if b.Output.Type == "image" {
|
||||
b.Yesf("%s finish building and pushing!\n", successfulIcon)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var errBuildAbort = errors.New("build aborted")
|
||||
|
||||
func (b *Builder) generateMetadata() error {
|
||||
// spec.yaml
|
||||
if b.isInterrupted() {
|
||||
return errBuildAbort
|
||||
}
|
||||
spec, err := os.Create(b.SpecYAMLPath())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer spec.Close()
|
||||
meta, err := ptypes.ParseGoSrc(b.ModelDir, b.Model)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = utils.MarshalYamlWithIndentTo(spec, meta, 2); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO(WeixinX): More languages need to be supported
|
||||
// README.md is required, README_{lang}.md is optional
|
||||
if b.isInterrupted() {
|
||||
return errBuildAbort
|
||||
}
|
||||
usages, err := meta.GetUsages()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to get wasm usage")
|
||||
}
|
||||
for i, u := range usages {
|
||||
// since `usages` are ordered by `I18nType` and currently only `en-US` and
|
||||
// `zh-CN` are available, en-US is the default README.md language when en-US is
|
||||
// present (because after sorting it is in the first place)
|
||||
suffix := true
|
||||
if i == 0 {
|
||||
suffix = false
|
||||
}
|
||||
if err = genMarkdownUsage(&u, b.tempDir, suffix); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Builder) imagePull(ctx context.Context) error {
|
||||
if b.isInterrupted() {
|
||||
return errBuildAbort
|
||||
}
|
||||
r, err := b.dockerCli.ImagePull(ctx, b.builderImageRef(), types.ImagePullOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if b.isInterrupted() {
|
||||
return errBuildAbort
|
||||
}
|
||||
io.Copy(b.w, r)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Builder) addContainerConfByOutType() error {
|
||||
if b.isInterrupted() {
|
||||
return errBuildAbort
|
||||
}
|
||||
|
||||
var err error
|
||||
switch b.Output.Type {
|
||||
case "files":
|
||||
err = b.filesHandler()
|
||||
case "image":
|
||||
err = b.imageHandler()
|
||||
default:
|
||||
return errors.New("invalid output option, output type is unknown")
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Builder) containerCreate(ctx context.Context) error {
|
||||
if b.isInterrupted() {
|
||||
return errBuildAbort
|
||||
}
|
||||
|
||||
resp, err := b.dockerCli.ContainerCreate(ctx, b.containerConf.Config, b.containerConf.HostConfig,
|
||||
b.containerConf.NetworkingConfig, b.containerConf.Platform, b.containerConf.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b.containerID = resp.ID
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Builder) containerStart(ctx context.Context) error {
|
||||
if b.isInterrupted() {
|
||||
return errBuildAbort
|
||||
}
|
||||
if err := b.dockerCli.ContainerStart(ctx, b.containerID, types.ContainerStartOptions{}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if b.isInterrupted() {
|
||||
return errBuildAbort
|
||||
}
|
||||
statusCh, errCh := b.dockerCli.ContainerWait(ctx, b.containerID, container.WaitConditionNotRunning)
|
||||
select {
|
||||
case err := <-errCh:
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case <-statusCh:
|
||||
}
|
||||
|
||||
if b.isInterrupted() {
|
||||
return errBuildAbort
|
||||
}
|
||||
logs, err := b.dockerCli.ContainerLogs(ctx, b.containerID, types.ContainerLogsOptions{ShowStdout: true, ShowStderr: true})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if b.isInterrupted() {
|
||||
return errBuildAbort
|
||||
}
|
||||
_, err = stdcopy.StdCopy(b.w, b.w, logs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var errWriteDockerEntrypoint = errors.New("failed to write docker entrypoint")
|
||||
|
||||
func (b *Builder) filesHandler() error {
|
||||
b.containerConf.HostConfig.Mounts = append(b.containerConf.HostConfig.Mounts, mount.Mount{
|
||||
// output dir for the build products
|
||||
Type: mount.TypeBind,
|
||||
Source: b.Output.Dest,
|
||||
Target: ContainerOutDir,
|
||||
})
|
||||
|
||||
ft := &FilesTmplFields{
|
||||
BuildSrcDir: ContainerWorkDir,
|
||||
BuildDestDir: ContainerTempDir,
|
||||
Output: ContainerOutDir,
|
||||
UID: b.uid,
|
||||
GID: b.uid,
|
||||
Debug: b.Debug,
|
||||
}
|
||||
|
||||
if err := genFilesDockerEntrypoint(ft, b.dockerEntrypoint); err != nil {
|
||||
return errors.Wrap(err, errWriteDockerEntrypoint.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
optionalProducts = [][2]string{
|
||||
{"README_ZH.md", MediaTypeREADME_ZH},
|
||||
{"README_EN.md", MediaTypeREADME_EN},
|
||||
{"icon.png", MediaTypeIcon},
|
||||
}
|
||||
)
|
||||
|
||||
// TODO(WeixinX): If the image exists, no push is performed
|
||||
func (b *Builder) imageHandler() error {
|
||||
products := ""
|
||||
for i, p := range optionalProducts {
|
||||
fileName := p[0]
|
||||
mediaType := p[1]
|
||||
if i == 0 {
|
||||
products = fmt.Sprintf("%s %s", fileName, mediaType)
|
||||
} else {
|
||||
products = fmt.Sprintf("%s %s %s", products, fileName, mediaType)
|
||||
}
|
||||
}
|
||||
|
||||
// spec.yaml, README.md and plugin.tar.gz are required
|
||||
basicCmd := fmt.Sprintf("oras push %s -u %s -p %s ./spec.yaml:%s ./README.md:%s",
|
||||
b.Output.Dest, b.Username, b.Password, MediaTypeSpec, MediaTypeREADME)
|
||||
|
||||
if b.Username == "" || b.Password == "" {
|
||||
basicCmd = fmt.Sprintf("oras push %s ./spec.yaml:%s ./README.md:%s",
|
||||
b.Output.Dest, MediaTypeSpec, MediaTypeREADME)
|
||||
|
||||
b.containerConf.HostConfig.Mounts = append(b.containerConf.HostConfig.Mounts, mount.Mount{
|
||||
// docker auth
|
||||
Type: mount.TypeBind,
|
||||
Source: b.DockerAuth,
|
||||
Target: ContainerDockerAuth,
|
||||
})
|
||||
}
|
||||
|
||||
it := &ImageTmplFields{
|
||||
BuildSrcDir: ContainerWorkDir,
|
||||
BuildDestDir: ContainerTempDir,
|
||||
Output: ContainerOutDir,
|
||||
Username: b.Username,
|
||||
Password: b.Password,
|
||||
BasicCmd: basicCmd,
|
||||
Products: products,
|
||||
MediaTypePlugin: MediaTypePlugin,
|
||||
Debug: b.Debug,
|
||||
}
|
||||
|
||||
if err := genImageDockerEntrypoint(it, b.dockerEntrypoint); err != nil {
|
||||
return errors.Wrap(err, errWriteDockerEntrypoint.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ConfigFunc is customized to set the fields of Builder
|
||||
type ConfigFunc func(b *Builder) error
|
||||
|
||||
func (b *Builder) config(f ConfigFunc) (err error) {
|
||||
if err = f(b); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// builder-go
|
||||
b.Builder.Go = strings.TrimSpace(b.Builder.Go)
|
||||
if b.Builder.Go == "" {
|
||||
b.Builder.Go = DefaultBuilderGo
|
||||
}
|
||||
|
||||
// builder-tinygo
|
||||
b.Builder.TinyGo = strings.TrimSpace(b.Builder.TinyGo)
|
||||
if b.Builder.TinyGo == "" {
|
||||
b.Builder.TinyGo = DefaultBuilderTinyGo
|
||||
}
|
||||
|
||||
// builder-oras
|
||||
b.Builder.Oras = strings.TrimSpace(b.Builder.Oras)
|
||||
if b.Builder.Oras == "" {
|
||||
b.Builder.Oras = DefaultBuilderOras
|
||||
}
|
||||
|
||||
// input
|
||||
b.Input = strings.TrimSpace(b.Input)
|
||||
if b.Input == "" {
|
||||
b.Input = "./"
|
||||
}
|
||||
inp, err := utils.GetAbsolutePath(b.Input)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to parse input option %q", b.Input)
|
||||
}
|
||||
b.Input = inp
|
||||
|
||||
// output-type
|
||||
b.Output.Type = strings.ToLower(strings.TrimSpace(b.Output.Type))
|
||||
if b.Output.Type == "" {
|
||||
b.Output.Type = "files"
|
||||
}
|
||||
if b.Output.Type != "files" && b.Output.Type != "image" {
|
||||
return errors.Errorf("invalid output type: %q, must be `files` or `image`", b.Output.Type)
|
||||
}
|
||||
|
||||
// output-dest
|
||||
b.Output.Dest = strings.TrimSpace(b.Output.Dest)
|
||||
if b.Output.Dest == "" {
|
||||
b.Output.Dest = "./out"
|
||||
}
|
||||
out := b.Output.Dest
|
||||
if b.Output.Type == "files" {
|
||||
out, err = utils.GetAbsolutePath(b.Output.Dest)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to parse output destination %q", b.Output.Dest)
|
||||
}
|
||||
err = os.MkdirAll(b.Output.Dest, 0755)
|
||||
if err != nil && !os.IsExist(err) {
|
||||
return errors.Wrapf(err, "failed to create output destination %q", b.Output.Dest)
|
||||
}
|
||||
}
|
||||
b.Output.Dest = out
|
||||
|
||||
// docker-auth
|
||||
b.DockerAuth = strings.TrimSpace(b.DockerAuth)
|
||||
if b.DockerAuth == "" {
|
||||
b.DockerAuth = "~/.docker/config.json"
|
||||
}
|
||||
auth, err := utils.GetAbsolutePath(b.DockerAuth)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to parse docker authentication %q", b.DockerAuth)
|
||||
}
|
||||
b.DockerAuth = auth
|
||||
|
||||
// model-dir
|
||||
b.ModelDir = strings.TrimSpace(b.ModelDir)
|
||||
if b.ModelDir == "" {
|
||||
b.ModelDir = "./"
|
||||
}
|
||||
|
||||
// option-file/username/password/model/debug: nothing to deal with
|
||||
|
||||
// the unexported fields that users do not need to care about are as follows:
|
||||
b.repository = DefaultBuilderRepository
|
||||
|
||||
b.tempDir, err = os.MkdirTemp("", HostTempDirPattern)
|
||||
if err != nil && !os.IsExist(err) {
|
||||
return errors.Wrap(err, "failed to create the host temporary dir")
|
||||
}
|
||||
|
||||
dockerEp, err := os.CreateTemp("", HostDockerEntryPattern)
|
||||
if err != nil && !os.IsExist(err) {
|
||||
return errors.Wrap(err, "failed to create the docker entrypoint file")
|
||||
}
|
||||
err = dockerEp.Chmod(0777)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b.dockerEntrypoint = dockerEp.Name()
|
||||
dockerEp.Close()
|
||||
|
||||
u, err := user.Current()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to get the current user information")
|
||||
}
|
||||
b.uid, b.gid = u.Uid, u.Gid
|
||||
|
||||
b.containerConf = types.ContainerCreateConfig{
|
||||
Name: "higress-wasm-go-builder",
|
||||
Config: &container.Config{
|
||||
Image: b.builderImageRef(),
|
||||
Env: []string{
|
||||
"GO111MODULE=on",
|
||||
"GOPROXY=https://goproxy.cn,direct",
|
||||
},
|
||||
WorkingDir: ContainerWorkDir,
|
||||
Entrypoint: []string{ContainerEntryFilePath},
|
||||
},
|
||||
HostConfig: &container.HostConfig{
|
||||
NetworkMode: "host",
|
||||
Mounts: []mount.Mount{
|
||||
{ // input dir that includes the wasm plugin source: main.go ...
|
||||
Type: mount.TypeBind,
|
||||
Source: b.Input,
|
||||
Target: ContainerWorkDir,
|
||||
},
|
||||
{ // temp dir that includes the wasm plugin metadata: spec.yaml and README.md ...
|
||||
Type: mount.TypeBind,
|
||||
Source: b.tempDir,
|
||||
Target: ContainerTempDir,
|
||||
},
|
||||
{ // entrypoint
|
||||
Type: mount.TypeBind,
|
||||
Source: b.dockerEntrypoint,
|
||||
Target: ContainerEntryFilePath,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if b.dockerCli == nil {
|
||||
b.dockerCli, err = client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to initialize the docker client")
|
||||
}
|
||||
}
|
||||
|
||||
if b.w == nil {
|
||||
b.w = os.Stdout
|
||||
}
|
||||
|
||||
b.sig = make(chan os.Signal, 1)
|
||||
b.stop = make(chan struct{}, 1)
|
||||
b.done = make(chan struct{}, 1)
|
||||
signal.Notify(b.sig, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM,
|
||||
syscall.SIGUSR1, syscall.SIGUSR2, syscall.SIGQUIT, syscall.SIGTSTP)
|
||||
|
||||
if b.Debugger == nil {
|
||||
b.Debugger = utils.NewDefaultDebugger(b.Debug, b.w)
|
||||
}
|
||||
|
||||
if b.YesOrNoPrinter == nil {
|
||||
b.YesOrNoPrinter = utils.NewPrinter(b.w, utils.DefaultIdent, utils.DefaultYes, utils.DefaultNo)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Builder) parseOptions(v *viper.Viper, cmd *cobra.Command) error {
|
||||
allOpt, err := option.ParseOptions(b.OptionFile, v, cmd.PersistentFlags())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b.BuildOptions = allOpt.Build
|
||||
|
||||
b.w = cmd.OutOrStdout()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Builder) finish() {
|
||||
select {
|
||||
case <-b.done:
|
||||
default:
|
||||
close(b.done)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Builder) waitForFinished() {
|
||||
<-b.done
|
||||
}
|
||||
|
||||
func (b *Builder) interrupt() {
|
||||
select {
|
||||
case <-b.stop:
|
||||
default:
|
||||
close(b.stop)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Builder) isInterrupted() bool {
|
||||
if b.stop == nil {
|
||||
return true
|
||||
}
|
||||
select {
|
||||
case <-b.stop:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// WithManualClean if set this option, then the temporary files and the container
|
||||
// will not be cleaned up automatically, and you need to clean up manually
|
||||
func (b *Builder) WithManualClean() {
|
||||
b.manualClean = true
|
||||
}
|
||||
|
||||
func (b *Builder) WithWriter(w io.Writer) {
|
||||
b.w = w
|
||||
}
|
||||
|
||||
// CleanupForError cleans up the temporary files and the container when an error occurs
|
||||
func (b *Builder) CleanupForError() {
|
||||
b.Cleanup()
|
||||
b.removeOutputDest()
|
||||
}
|
||||
|
||||
// Cleanup cleans up the temporary files and the container
|
||||
func (b *Builder) Cleanup() {
|
||||
b.removeTempDir()
|
||||
b.removeDockerEntrypoint()
|
||||
b.removeBuilderContainer()
|
||||
b.closeDockerCli()
|
||||
}
|
||||
|
||||
func (b *Builder) removeOutputDest() {
|
||||
if b.BuildOptions.Output.Type == "files" {
|
||||
b.Debugf("remove output destination %q\n", b.BuildOptions.Output.Dest)
|
||||
os.RemoveAll(b.BuildOptions.Output.Dest)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Builder) removeTempDir() {
|
||||
if b.tempDir != "" {
|
||||
b.Debugf("remove temporary directory %q\n", b.tempDir)
|
||||
os.RemoveAll(b.tempDir)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Builder) removeDockerEntrypoint() {
|
||||
if b.dockerEntrypoint != "" {
|
||||
b.Debugf("delete docker entrypoint %q\n", b.dockerEntrypoint)
|
||||
os.Remove(b.dockerEntrypoint)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Builder) removeBuilderContainer() {
|
||||
if b.containerID != "" {
|
||||
err := b.dockerCli.ContainerRemove(context.TODO(), b.containerID, types.ContainerRemoveOptions{Force: true})
|
||||
if err != nil {
|
||||
b.Debugf("failed to remove container (%s): %s\n", b.containerConf.Name, b.containerID)
|
||||
} else {
|
||||
b.Debugf("remove container (%s): %s\n", b.containerConf.Name, b.containerID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Builder) closeDockerCli() {
|
||||
if b.dockerCli != nil {
|
||||
b.Debugln("close the docker client")
|
||||
b.dockerCli.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Builder) builderImageRef() string {
|
||||
return fmt.Sprintf("%s:go%s-tinygo%s-oras%s", b.repository, b.Builder.Go, b.Builder.TinyGo, b.Builder.Oras)
|
||||
}
|
||||
|
||||
func (b *Builder) SpecYAMLPath() string {
|
||||
return fmt.Sprintf("%s/spec.yaml", b.tempDir)
|
||||
}
|
||||
|
||||
func (b *Builder) TempDir() string {
|
||||
return b.tempDir
|
||||
}
|
||||
|
||||
func (b *Builder) String() string {
|
||||
by, err := json.MarshalIndent(b, "", " ")
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return string(by)
|
||||
}
|
||||
194
pkg/cmd/hgctl/plugin/build/templates.go
Normal file
194
pkg/cmd/hgctl/plugin/build/templates.go
Normal file
@@ -0,0 +1,194 @@
|
||||
// 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 build
|
||||
|
||||
import (
|
||||
"os"
|
||||
"text/template"
|
||||
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/plugin/types"
|
||||
)
|
||||
|
||||
const (
|
||||
filesDockerEntrypoint = `#!/bin/bash
|
||||
set -e
|
||||
{{- if eq .Debug true }}
|
||||
set -x
|
||||
{{- end }}
|
||||
|
||||
go mod tidy
|
||||
tinygo build -o {{ .BuildDestDir }}/plugin.wasm -scheduler=none -gc=custom -tags='custommalloc nottinygc_finalizer' -target=wasi {{ .BuildSrcDir }}
|
||||
|
||||
mv {{ .BuildDestDir }}/* {{ .Output }}/
|
||||
chown -R {{ .UID }}:{{ .GID }} {{ .Output }}
|
||||
`
|
||||
imageDockerEntrypoint = `#!/bin/bash
|
||||
set -e
|
||||
{{- if eq .Debug true }}
|
||||
set -x
|
||||
{{- end }}
|
||||
|
||||
go mod tidy
|
||||
tinygo build -o {{ .BuildDestDir }}/plugin.wasm -scheduler=none -gc=custom -tags='custommalloc nottinygc_finalizer' -target=wasi {{ .BuildSrcDir }}
|
||||
|
||||
cd {{ .BuildDestDir }}
|
||||
tar czf plugin.tar.gz plugin.wasm
|
||||
cmd="{{ .BasicCmd }}"
|
||||
products=({{ .Products }})
|
||||
for ((i=0; i<${#products[*]}; i=i+2)); do
|
||||
f=${products[i]}
|
||||
typ=${products[i+1]}
|
||||
if [ -e ${f} ]; then
|
||||
cmd="${cmd} ./${f}:${typ}"
|
||||
fi
|
||||
done
|
||||
cmd="${cmd} ./plugin.tar.gz:{{ .MediaTypePlugin }}"
|
||||
eval ${cmd}
|
||||
`
|
||||
)
|
||||
|
||||
type FilesTmplFields struct {
|
||||
BuildSrcDir string
|
||||
BuildDestDir string
|
||||
Output string
|
||||
UID, GID string
|
||||
Debug bool
|
||||
}
|
||||
|
||||
type ImageTmplFields struct {
|
||||
BuildSrcDir string
|
||||
BuildDestDir string
|
||||
Output string
|
||||
Username, Password string
|
||||
BasicCmd string
|
||||
Products string
|
||||
MediaTypePlugin string
|
||||
Debug bool
|
||||
}
|
||||
|
||||
func genFilesDockerEntrypoint(ft *FilesTmplFields, target string) error {
|
||||
f, err := os.OpenFile(target, os.O_CREATE|os.O_WRONLY, 0777)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if err = template.Must(template.New("FilesDockerEntrypoint").Parse(filesDockerEntrypoint)).Execute(f, ft); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func genImageDockerEntrypoint(it *ImageTmplFields, target string) error {
|
||||
f, err := os.OpenFile(target, os.O_CREATE|os.O_WRONLY, 0777)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if err = template.Must(template.New("ImageDockerEntrypoint").Parse(imageDockerEntrypoint)).Execute(f, it); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
readme_zh_CN = `> 该插件用法文件根据源代码自动生成,请根据需求自行修改!
|
||||
|
||||
# 功能说明
|
||||
|
||||
{{ .Description }}
|
||||
|
||||
# 配置字段
|
||||
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
| -------- | -------- | -------- | -------- | -------- |
|
||||
{{- range .ConfigEntries }}
|
||||
| {{ .Name }} | {{ .Type }} | {{ .Requirement }} | {{ .Default }} | {{ .Description }} |
|
||||
{{- end }}
|
||||
|
||||
# 配置示例
|
||||
|
||||
` + "```yaml" + `
|
||||
{{ .Example }}
|
||||
` + "```" + `
|
||||
`
|
||||
|
||||
readme_en_US = `> THIS PLUGIN USAGE FILE IS AUTOMATICALLY GENERATED BASED ON THE SOURCE CODE. MODIFY IT AS REQUIRED!
|
||||
|
||||
# Description
|
||||
|
||||
{{ .Description }}
|
||||
|
||||
# Configuration
|
||||
|
||||
| Name | Type | Requirement | Default | Description |
|
||||
| -------- | -------- | -------- | -------- | -------- |
|
||||
{{- range .ConfigEntries }}
|
||||
| {{ .Name }} | {{ .Type }} | {{ .Requirement }} | {{ .Default }} | {{ .Description }} |
|
||||
{{- end }}
|
||||
|
||||
# Examples
|
||||
|
||||
` + "```yaml" + `
|
||||
{{ .Example }}
|
||||
` + "```" + `
|
||||
`
|
||||
)
|
||||
|
||||
func genMarkdownUsage(u *types.WasmUsage, dir string, suffix bool) error {
|
||||
md, err := os.Create(i18n2MDTitle(u.I18nType, dir, suffix))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer md.Close()
|
||||
|
||||
if err = template.Must(template.New("MD_Usage").Parse(i18n2MD(u.I18nType))).Execute(md, u); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func i18n2MD(i18n types.I18nType) string {
|
||||
switch i18n {
|
||||
case types.I18nEN_US:
|
||||
return readme_en_US
|
||||
case types.I18nZH_CN:
|
||||
return readme_zh_CN
|
||||
default:
|
||||
return readme_zh_CN
|
||||
}
|
||||
}
|
||||
|
||||
func i18n2MDTitle(i18n types.I18nType, dir string, suffix bool) string {
|
||||
var file string
|
||||
if !suffix {
|
||||
file = "README.md"
|
||||
} else {
|
||||
switch i18n {
|
||||
case types.I18nEN_US:
|
||||
file = "README_EN.md"
|
||||
case types.I18nZH_CN:
|
||||
file = "README_ZH.md"
|
||||
default:
|
||||
file = "README_ZH.md"
|
||||
}
|
||||
}
|
||||
|
||||
return dir + "/" + file
|
||||
}
|
||||
30
pkg/cmd/hgctl/plugin/config/config.go
Normal file
30
pkg/cmd/hgctl/plugin/config/config.go
Normal file
@@ -0,0 +1,30 @@
|
||||
// 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 config
|
||||
|
||||
import "github.com/spf13/cobra"
|
||||
|
||||
func NewCommand() *cobra.Command {
|
||||
configCmd := &cobra.Command{
|
||||
Use: "config",
|
||||
Aliases: []string{"conf", "cnf"},
|
||||
Short: "Configure the WasmPlugin manifest",
|
||||
}
|
||||
|
||||
configCmd.AddCommand(newCreateCommand())
|
||||
configCmd.AddCommand(newEditCommand())
|
||||
|
||||
return configCmd
|
||||
}
|
||||
76
pkg/cmd/hgctl/plugin/config/create.go
Normal file
76
pkg/cmd/hgctl/plugin/config/create.go
Normal file
@@ -0,0 +1,76 @@
|
||||
// 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 config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/plugin/utils"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
)
|
||||
|
||||
func newCreateCommand() *cobra.Command {
|
||||
var target string
|
||||
|
||||
createCmd := &cobra.Command{
|
||||
Use: "create",
|
||||
Aliases: []string{"c"},
|
||||
Short: "Create the WASM plugin configuration template file",
|
||||
Example: ` hgctl plugin config create`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.CheckErr(create(cmd.OutOrStdout(), target))
|
||||
},
|
||||
}
|
||||
|
||||
createCmd.PersistentFlags().StringVarP(&target, "target", "t", "./", "Directory where the configuration is generated")
|
||||
|
||||
return createCmd
|
||||
}
|
||||
|
||||
func create(w io.Writer, target string) error {
|
||||
target, err := utils.GetAbsolutePath(target)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "invalid target path")
|
||||
}
|
||||
if err = os.MkdirAll(target, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = GenPluginConfYAML(configHelpTmpl, target); err != nil {
|
||||
return errors.Wrap(err, "failed to create configuration template")
|
||||
}
|
||||
|
||||
fmt.Fprintf(w, "Created configuration template %q\n", fmt.Sprintf("%s/%s", target, "plugin-conf.yaml"))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var configHelpTmpl = &PluginConf{
|
||||
Name: "Plugin Name",
|
||||
Namespace: "higress-system",
|
||||
Title: "Display Name",
|
||||
Description: "Plugin Description",
|
||||
IconUrl: "Plugin Icon",
|
||||
Version: "0.1.0",
|
||||
Category: "auth | security | protocol | flow-control | flow-monitor | custom",
|
||||
Phase: "UNSPECIFIED_PHASE | AUTHN | AUTHZ | STATS",
|
||||
Priority: 0,
|
||||
Config: " Plugin Configuration",
|
||||
Url: "Plugin Image URL",
|
||||
}
|
||||
143
pkg/cmd/hgctl/plugin/config/edit.go
Normal file
143
pkg/cmd/hgctl/plugin/config/edit.go
Normal file
@@ -0,0 +1,143 @@
|
||||
// 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 config
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
k8s "github.com/alibaba/higress/pkg/cmd/hgctl/kubernetes"
|
||||
"github.com/alibaba/higress/pkg/cmd/options"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
k8serr "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/util/yaml"
|
||||
"k8s.io/cli-runtime/pkg/printers"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
"k8s.io/kubectl/pkg/cmd/util/editor"
|
||||
)
|
||||
|
||||
func newEditCommand() *cobra.Command {
|
||||
var name string
|
||||
|
||||
editCmd := &cobra.Command{
|
||||
Use: "edit",
|
||||
Aliases: []string{"e"},
|
||||
Short: "Edit the installed WASM plugin configuration",
|
||||
Example: ` # Edit the installed WASM plugin 'request-block'
|
||||
hgctl plugin config edit -p request-block
|
||||
`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.CheckErr(edit(cmd.OutOrStdout(), name))
|
||||
},
|
||||
}
|
||||
|
||||
flags := editCmd.PersistentFlags()
|
||||
options.AddKubeConfigFlags(flags)
|
||||
k8s.AddHigressNamespaceFlags(flags)
|
||||
flags.StringVarP(&name, "name", "p", "", "Name of the WASM plugin that needs to be edited")
|
||||
|
||||
return editCmd
|
||||
}
|
||||
|
||||
func edit(w io.Writer, name string) error {
|
||||
// TODO(WeixinX): Use WasmPlugin Object type instead of Unstructured
|
||||
dynCli, err := k8s.NewDynamicClient(options.DefaultConfigFlags.ToRawKubeConfigLoader())
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to build kubernetes dynamic client")
|
||||
}
|
||||
cli := k8s.NewWasmPluginClient(dynCli)
|
||||
|
||||
originalObj, err := cli.Get(context.TODO(), name)
|
||||
if err != nil {
|
||||
if k8serr.IsNotFound(err) {
|
||||
return errors.Errorf("wasm plugin %q is not found", fmt.Sprintf("%s/%s", k8s.HigressNamespace, name))
|
||||
}
|
||||
return errors.Wrapf(err, "failed to get wasm plugin %q", fmt.Sprintf("%s/%s", k8s.HigressNamespace, name))
|
||||
}
|
||||
|
||||
originalObj.SetGroupVersionKind(k8s.WasmPluginGVK)
|
||||
originalObj.SetManagedFields(nil) // TODO(WeixinX): Managed Fields should be written back
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
var wObj io.Writer = buf
|
||||
printer := printers.YAMLPrinter{}
|
||||
if err = printer.PrintObj(originalObj.DeepCopyObject(), wObj); err != nil {
|
||||
return err
|
||||
}
|
||||
original := buf.Bytes()
|
||||
e := editor.NewDefaultEditor(editorEnvs())
|
||||
edited, file, err := e.LaunchTempFile("higress-wasm-edit-", ".yaml", buf)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to launch editor")
|
||||
}
|
||||
defer os.Remove(file)
|
||||
|
||||
if bytes.Equal(cmdutil.StripComments(original), cmdutil.StripComments(edited)) { // no change
|
||||
fmt.Fprintf(w, "edit %q canceled, no change\n",
|
||||
fmt.Sprintf("%s/%s", originalObj.GetNamespace(), originalObj.GetName()))
|
||||
return nil
|
||||
}
|
||||
|
||||
var editedObj unstructured.Unstructured
|
||||
eBuf := bytes.NewReader(edited)
|
||||
dc := yaml.NewYAMLOrJSONDecoder(eBuf, 4096)
|
||||
if err = dc.Decode(&editedObj); err != nil {
|
||||
return err
|
||||
}
|
||||
if !keepSameMeta(&editedObj, originalObj) {
|
||||
fmt.Fprintln(w, "Warning: ensure that the apiVersion, kind, namespace, and name are the same as the original and are automatically corrected")
|
||||
}
|
||||
|
||||
ret, err := cli.Update(context.TODO(), &editedObj)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to update wasm plugin %q",
|
||||
fmt.Sprintf("%s/%s", originalObj.GetNamespace(), originalObj.GetName()))
|
||||
}
|
||||
|
||||
fmt.Fprintf(w, "Edited wasm plugin %q\n", fmt.Sprintf("%s/%s", ret.GetNamespace(), ret.GetName()))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func editorEnvs() []string {
|
||||
return []string{
|
||||
"KUBE_EDITOR",
|
||||
"EDITOR",
|
||||
}
|
||||
}
|
||||
|
||||
// to avoid changing the apiVersion, kind, namespace and name, keep them the same as the original
|
||||
func keepSameMeta(edited, original *unstructured.Unstructured) bool {
|
||||
same := true
|
||||
if edited.GroupVersionKind().String() != original.GroupVersionKind().String() {
|
||||
edited.SetGroupVersionKind(original.GroupVersionKind())
|
||||
same = false
|
||||
}
|
||||
if edited.GetNamespace() != original.GetNamespace() {
|
||||
edited.SetNamespace(original.GetNamespace())
|
||||
same = false
|
||||
}
|
||||
if edited.GetName() != original.GetName() {
|
||||
edited.SetName(original.GetName())
|
||||
same = false
|
||||
}
|
||||
return same
|
||||
}
|
||||
160
pkg/cmd/hgctl/plugin/config/templates.go
Normal file
160
pkg/cmd/hgctl/plugin/config/templates.go
Normal file
@@ -0,0 +1,160 @@
|
||||
// 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 config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/plugin/types"
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/plugin/utils"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// TODO(WeixinX): Use 'hgctl plugin push' command to fill the image url automatically
|
||||
const pluginConfYAML = `# File generated by hgctl. Modify as required.
|
||||
# See: https://higress.io/zh-cn/docs/plugins/intro
|
||||
|
||||
apiVersion: extensions.higress.io/v1alpha1
|
||||
kind: WasmPlugin
|
||||
metadata:
|
||||
name: {{ .Name }}
|
||||
namespace: {{ .Namespace }}
|
||||
annotations:
|
||||
higress.io/wasm-plugin-title: {{ .Title }}
|
||||
higress.io/wasm-plugin-description: {{ .Description }}
|
||||
higress.io/wasm-plugin-icon: {{ .IconUrl }}
|
||||
labels:
|
||||
higress.io/wasm-plugin-name: {{ .Name }}
|
||||
higress.io/wasm-plugin-category: {{ .Category }}
|
||||
higress.io/wasm-plugin-version: {{ .Version }}
|
||||
higress.io/resource-definer: higress
|
||||
higress.io/wasm-plugin-built-in: "false"
|
||||
spec:
|
||||
phase: {{ .Phase }}
|
||||
priority: {{ .Priority }}
|
||||
{{ .Config }}
|
||||
# Please fill the image url in according to your needs
|
||||
url: {{ .Url }}
|
||||
`
|
||||
|
||||
type PluginConf struct {
|
||||
Name string
|
||||
Namespace string
|
||||
Title string
|
||||
Description string
|
||||
IconUrl string
|
||||
Version string
|
||||
Category string
|
||||
Phase string
|
||||
Priority int64
|
||||
Config string
|
||||
Url string
|
||||
}
|
||||
|
||||
func (pc *PluginConf) String() string {
|
||||
b, err := json.MarshalIndent(pc, "", " ")
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// GenPluginConfYAML generates plugin-conf.yaml based on the template
|
||||
func GenPluginConfYAML(p *PluginConf, dir string) error {
|
||||
path := fmt.Sprintf("%s/plugin-conf.yaml", dir)
|
||||
f, err := os.Create(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if err = template.Must(template.New("PluginConfYAML").Parse(pluginConfYAML)).Execute(f, p); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ExtractPluginConfFrom extracts the params of plugin-conf.yaml from spec.yaml.
|
||||
// input params `config`, `url` are only used to implement the command `hgctl plugin install -g <go-project>`
|
||||
func ExtractPluginConfFrom(spec *types.WasmPluginMeta, config, url string) (*PluginConf, error) {
|
||||
if config == "" {
|
||||
// by default, Example from spec.yaml is used as the defaultConfig for the wasm plugin
|
||||
var obj map[string]interface{}
|
||||
example := spec.GetConfigExample()
|
||||
if err := yaml.Unmarshal([]byte(example), &obj); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
conf := struct {
|
||||
DefaultConfig map[string]interface{} `yaml:"defaultConfig,omitempty"`
|
||||
}{DefaultConfig: obj}
|
||||
b, err := utils.MarshalYamlWithIndent(conf, 2)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config = string(b)
|
||||
}
|
||||
|
||||
pc := &PluginConf{
|
||||
Name: spec.Info.Name,
|
||||
Namespace: "higress-system",
|
||||
Title: spec.Info.Title,
|
||||
Description: spec.Info.Description,
|
||||
IconUrl: spec.Info.IconUrl,
|
||||
Version: spec.Info.Version,
|
||||
Category: string(spec.Info.Category),
|
||||
Phase: string(spec.Spec.Phase),
|
||||
Priority: spec.Spec.Priority,
|
||||
Config: utils.AddIndent(config, strings.Repeat(" ", 2)),
|
||||
Url: url,
|
||||
}
|
||||
pc.withDefaultValue()
|
||||
|
||||
return pc, nil
|
||||
}
|
||||
|
||||
func (pc *PluginConf) withDefaultValue() {
|
||||
if pc.Name == "" {
|
||||
pc.Name = "Unnamed"
|
||||
}
|
||||
if pc.Namespace == "" {
|
||||
pc.Namespace = "higress-system"
|
||||
}
|
||||
if pc.Title == "" {
|
||||
pc.Title = "Untitled"
|
||||
}
|
||||
if pc.Description == "" {
|
||||
pc.Description = "No description"
|
||||
}
|
||||
if pc.IconUrl == "" {
|
||||
pc.IconUrl = types.Category2IconUrl(types.Category(pc.Category))
|
||||
}
|
||||
if pc.Version == "" {
|
||||
pc.Version = "0.1.0"
|
||||
}
|
||||
if pc.Category == "" {
|
||||
pc.Category = string(types.CategoryDefault)
|
||||
}
|
||||
if pc.Phase == "" {
|
||||
pc.Phase = string(types.PhaseDefault)
|
||||
}
|
||||
|
||||
}
|
||||
92
pkg/cmd/hgctl/plugin/init/init.go
Normal file
92
pkg/cmd/hgctl/plugin/init/init.go
Normal file
@@ -0,0 +1,92 @@
|
||||
// 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 plugininit
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/plugin/option"
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/plugin/utils"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2/terminal"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
)
|
||||
|
||||
func NewCommand() *cobra.Command {
|
||||
var target string
|
||||
|
||||
initCmd := &cobra.Command{
|
||||
Use: "init",
|
||||
Aliases: []string{"ini", "i"},
|
||||
Short: "Initialize a Golang WASM plugin project",
|
||||
Example: ` hgctl plugin init`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.CheckErr(runInit(cmd.OutOrStdout(), target))
|
||||
},
|
||||
}
|
||||
|
||||
initCmd.PersistentFlags().StringVarP(&target, "target", "t", "./", "Directory where the project is initialized")
|
||||
|
||||
return initCmd
|
||||
}
|
||||
|
||||
func runInit(w io.Writer, target string) (err error) {
|
||||
ans := answer{}
|
||||
err = utils.Ask(questions, &ans)
|
||||
if err != nil {
|
||||
if errors.Is(err, terminal.InterruptErr) {
|
||||
fmt.Fprintf(w, "Interrupted\n")
|
||||
return nil
|
||||
}
|
||||
return errors.Wrap(err, "failed to initialize the project")
|
||||
}
|
||||
|
||||
target, err = utils.GetAbsolutePath(target)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "invalid target directory")
|
||||
}
|
||||
dir := fmt.Sprintf("%s/%s", target, ans.Name)
|
||||
err = os.MkdirAll(dir, 0755)
|
||||
defer func() {
|
||||
if err != nil {
|
||||
os.RemoveAll(dir)
|
||||
err = errors.Wrap(err, "failed to initialize the project")
|
||||
|
||||
}
|
||||
}()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if err = genGoMain(&ans, dir); err != nil {
|
||||
return errors.Wrap(err, "failed to create main.go")
|
||||
}
|
||||
if err = genGoMod(&ans, dir); err != nil {
|
||||
return errors.Wrap(err, "failed to create go.mod")
|
||||
}
|
||||
if err = genGitIgnore(dir); err != nil {
|
||||
return errors.Wrap(err, "failed to create .gitignore")
|
||||
}
|
||||
if err = option.GenOptionYAML(dir); err != nil {
|
||||
return errors.Wrap(err, "failed to create option.yaml")
|
||||
}
|
||||
|
||||
fmt.Fprintf(w, "Initialized the project in %q\n", dir)
|
||||
|
||||
return nil
|
||||
}
|
||||
296
pkg/cmd/hgctl/plugin/init/templates.go
Normal file
296
pkg/cmd/hgctl/plugin/init/templates.go
Normal file
@@ -0,0 +1,296 @@
|
||||
// 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 plugininit
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"text/template"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/plugin/types"
|
||||
)
|
||||
|
||||
const (
|
||||
goMain = `// File generated by hgctl. Modify as required.
|
||||
// See: https://higress.io/zh-cn/docs/user/wasm-go#2-%E7%BC%96%E5%86%99-maingo-%E6%96%87%E4%BB%B6
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/tidwall/gjson"
|
||||
"github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm"
|
||||
"github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm/types"
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
)
|
||||
|
||||
func main() {
|
||||
wrapper.SetCtx(
|
||||
"{{ .Name }}",
|
||||
wrapper.ParseConfigBy(parseConfig),
|
||||
wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
|
||||
)
|
||||
}
|
||||
|
||||
// @Name {{ .Name }}
|
||||
// @Category {{ .Category }}
|
||||
// @Phase {{ .Phase }}
|
||||
// @Priority {{ .Priority }}
|
||||
// @Title {{ .I18nType }} {{ .Title }}
|
||||
// @Description {{ .I18nType }} {{ .Description }}
|
||||
// @IconUrl {{ .IconUrl }}
|
||||
// @Version {{ .Version }}
|
||||
//
|
||||
// @Contact.name {{ .ContactName }}
|
||||
// @Contact.url {{ .ContactUrl }}
|
||||
// @Contact.email {{ .ContactEmail }}
|
||||
//
|
||||
// @Example
|
||||
// firstField: hello
|
||||
// secondField: world
|
||||
// @End
|
||||
//
|
||||
type PluginConfig struct {
|
||||
// @Title 第一个字段,注解格式为 @Title [语言] [标题],语言缺省值为 en-US
|
||||
// @Description 字符串的前半部分,注解格式为 @Description [语言] [描述],语言缺省值为 en-US
|
||||
firstField string ` + "`required:\"true\"`" + `
|
||||
|
||||
// @Title en-US Second Field, annotation format is @Title [language] [title], language defaults to en-US
|
||||
// @Description en-US The second half of the string, annotation format is @Description [language] [description], language defaults to en-US
|
||||
secondField string ` + "`required:\"true\"`" + `
|
||||
}
|
||||
|
||||
func parseConfig(json gjson.Result, config *PluginConfig, log wrapper.Log) error {
|
||||
config.firstField = json.Get("firstField").String()
|
||||
config.secondField = json.Get("secondField").String()
|
||||
return nil
|
||||
}
|
||||
|
||||
func onHttpRequestHeaders(ctx wrapper.HttpContext, config PluginConfig, log wrapper.Log) types.Action {
|
||||
err := proxywasm.AddHttpRequestHeader(config.firstField, config.secondField)
|
||||
if err != nil {
|
||||
log.Critical("failed to set request header")
|
||||
}
|
||||
return types.ActionContinue
|
||||
}
|
||||
`
|
||||
goMod = `// File generated by hgctl. Modify as required.
|
||||
|
||||
module {{ .Name }}
|
||||
|
||||
go 1.19
|
||||
|
||||
require (
|
||||
github.com/alibaba/higress/plugins/wasm-go v0.0.0-20231019123123-86b223bc75f1
|
||||
github.com/tetratelabs/proxy-wasm-go-sdk v0.22.0
|
||||
github.com/tidwall/gjson v1.14.3
|
||||
)
|
||||
`
|
||||
|
||||
gitIgnore = `# File generated by hgctl. Modify as required.
|
||||
|
||||
*
|
||||
|
||||
!/.gitignore
|
||||
|
||||
!*.go
|
||||
!go.sum
|
||||
!go.mod
|
||||
|
||||
!LICENSE
|
||||
!*.md
|
||||
!*.yaml
|
||||
!*.yml
|
||||
|
||||
!*/
|
||||
|
||||
/out
|
||||
/test
|
||||
`
|
||||
)
|
||||
|
||||
func genGoMain(ans *answer, dir string) error {
|
||||
path := fmt.Sprintf("%s/main.go", dir)
|
||||
f, err := os.Create(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if err = template.Must(template.New("GoMain").Parse(goMain)).Execute(f, ans); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func genGoMod(ans *answer, dir string) error {
|
||||
path := fmt.Sprintf("%s/go.mod", dir)
|
||||
f, err := os.Create(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if err = template.Must(template.New("GoMod").Parse(goMod)).Execute(f, ans); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func genGitIgnore(dir string) error {
|
||||
path := fmt.Sprintf("%s/.gitignore", dir)
|
||||
f, err := os.Create(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if _, err = f.WriteString(gitIgnore); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// obtain parameters through command line interaction
|
||||
type answer struct {
|
||||
Name string
|
||||
Category string
|
||||
Phase string
|
||||
Priority int64
|
||||
I18nType string
|
||||
Title string
|
||||
Description string
|
||||
IconUrl string
|
||||
Version string
|
||||
|
||||
ContactName string
|
||||
ContactUrl string
|
||||
ContactEmail string
|
||||
}
|
||||
|
||||
var questions = []*survey.Question{
|
||||
{
|
||||
Name: "Name",
|
||||
Prompt: &survey.Input{
|
||||
Message: "Plugin name:",
|
||||
Default: "hello-world",
|
||||
},
|
||||
Validate: survey.Required,
|
||||
},
|
||||
{
|
||||
Name: "Category",
|
||||
Prompt: &survey.Select{
|
||||
Message: "Choose a plugin category:",
|
||||
Options: []string{
|
||||
string(types.CategoryCustom),
|
||||
string(types.CategoryAuth),
|
||||
string(types.CategorySecurity),
|
||||
string(types.CategoryProtocol),
|
||||
string(types.CategoryFlowControl),
|
||||
string(types.CategoryFlowMonitor),
|
||||
},
|
||||
Default: string(types.CategoryCustom),
|
||||
},
|
||||
Validate: survey.Required,
|
||||
},
|
||||
{
|
||||
Name: "Phase",
|
||||
Prompt: &survey.Select{
|
||||
Message: "Choose a execution phase:",
|
||||
Options: []string{
|
||||
string(types.PhaseUnspecified),
|
||||
string(types.PhaseAuthn),
|
||||
string(types.PhaseAuthz),
|
||||
string(types.PhaseStats),
|
||||
},
|
||||
Default: string(types.PhaseUnspecified),
|
||||
},
|
||||
Validate: survey.Required,
|
||||
},
|
||||
{
|
||||
Name: "Priority",
|
||||
Prompt: &survey.Input{
|
||||
Message: "Execution priority:",
|
||||
Default: "0",
|
||||
},
|
||||
Validate: survey.Required,
|
||||
},
|
||||
{
|
||||
Name: "I18nType",
|
||||
Prompt: &survey.Select{
|
||||
Message: "Choose a language:",
|
||||
Options: []string{
|
||||
string(types.I18nEN_US),
|
||||
string(types.I18nZH_CN),
|
||||
},
|
||||
Default: string(types.I18nDefault),
|
||||
},
|
||||
Validate: survey.Required,
|
||||
},
|
||||
{
|
||||
Name: "Title",
|
||||
Prompt: &survey.Input{
|
||||
Message: "Display name in the plugin market:",
|
||||
Default: "Hello World",
|
||||
},
|
||||
Validate: survey.Required,
|
||||
},
|
||||
{
|
||||
Name: "Description",
|
||||
Prompt: &survey.Input{
|
||||
Message: "Description of the plugin functionality:",
|
||||
Default: "This is a demo plugin",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "IconUrl",
|
||||
Prompt: &survey.Input{
|
||||
Message: "Display icon in the plugin market:",
|
||||
Default: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Version",
|
||||
Prompt: &survey.Input{
|
||||
Message: "Plugin version:",
|
||||
Default: "0.1.0",
|
||||
},
|
||||
Validate: survey.Required,
|
||||
},
|
||||
{
|
||||
Name: "ContactName",
|
||||
Prompt: &survey.Input{
|
||||
Message: "Name of developer:",
|
||||
Default: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "ContactUrl",
|
||||
Prompt: &survey.Input{
|
||||
Message: "Homepage of developer:",
|
||||
Default: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "ContactEmail",
|
||||
Prompt: &survey.Input{
|
||||
Message: "Email of developer:",
|
||||
Default: "",
|
||||
},
|
||||
},
|
||||
}
|
||||
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))
|
||||
}
|
||||
78
pkg/cmd/hgctl/plugin/ls/ls.go
Normal file
78
pkg/cmd/hgctl/plugin/ls/ls.go
Normal file
@@ -0,0 +1,78 @@
|
||||
// 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 ls
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
k8s "github.com/alibaba/higress/pkg/cmd/hgctl/kubernetes"
|
||||
"github.com/alibaba/higress/pkg/cmd/options"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/apimachinery/pkg/util/duration"
|
||||
"k8s.io/cli-runtime/pkg/printers"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
)
|
||||
|
||||
func NewCommand() *cobra.Command {
|
||||
lsCmd := &cobra.Command{
|
||||
Use: "ls",
|
||||
Aliases: []string{"l"},
|
||||
Short: "List all installed WASM plugins",
|
||||
Example: ` hgctl plugin ls`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.CheckErr(runLs(cmd.OutOrStdout()))
|
||||
},
|
||||
}
|
||||
|
||||
flags := lsCmd.PersistentFlags()
|
||||
options.AddKubeConfigFlags(flags)
|
||||
k8s.AddHigressNamespaceFlags(flags)
|
||||
|
||||
return lsCmd
|
||||
}
|
||||
|
||||
func runLs(w io.Writer) error {
|
||||
dynCli, err := k8s.NewDynamicClient(options.DefaultConfigFlags.ToRawKubeConfigLoader())
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to build kubernetes client")
|
||||
}
|
||||
cli := k8s.NewWasmPluginClient(dynCli)
|
||||
|
||||
list, err := cli.List(context.TODO())
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to list all wasm plugins")
|
||||
}
|
||||
|
||||
printer := printers.GetNewTabWriter(w)
|
||||
now := time.Now()
|
||||
fmt.Fprintf(printer, "NAME\tAGE\n")
|
||||
for _, item := range list.Items {
|
||||
fmt.Fprintf(printer, "%s\t%s\n", item.GetName(), getAge(now, item.GetCreationTimestamp().Time))
|
||||
}
|
||||
if err = printer.Flush(); err != nil {
|
||||
return errors.Wrap(err, "failed to flush output")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getAge(now time.Time, create time.Time) string {
|
||||
return duration.ShortHumanDuration(now.Sub(create))
|
||||
}
|
||||
96
pkg/cmd/hgctl/plugin/option/option.go
Normal file
96
pkg/cmd/hgctl/plugin/option/option.go
Normal file
@@ -0,0 +1,96 @@
|
||||
// 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 option
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/pflag"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type Option struct {
|
||||
Version string `json:"version" yaml:"version" mapstructure:"version"`
|
||||
Build BuildOptions `json:"build" yaml:"build" mapstructure:"build"`
|
||||
Test TestOptions `json:"test" yaml:"test" mapstructure:"test"`
|
||||
Install InstallOptions `json:"install" yaml:"install" mapstructure:"install"`
|
||||
}
|
||||
|
||||
type BuildOptions struct {
|
||||
Builder BuilderVersion `json:"builder" yaml:"builder" mapstructure:"builder"`
|
||||
Input string `json:"input" yaml:"input" mapstructure:"input"`
|
||||
Output Output `json:"output" yaml:"output" mapstructure:"output"`
|
||||
DockerAuth string `json:"docker-auth" yaml:"docker-auth" mapstructure:"docker-auth"`
|
||||
ModelDir string `json:"model-dir" yaml:"model-dir" mapstructure:"model-dir"`
|
||||
Model string `json:"model" yaml:"model" mapstructure:"model"`
|
||||
Debug bool `json:"debug" yaml:"debug" mapstructure:"debug"`
|
||||
}
|
||||
|
||||
type TestOptions struct {
|
||||
Name string `json:"name" yaml:"name" mapstructure:"name"`
|
||||
FromPath string `json:"from-path" yaml:"from-path" mapstructure:"from-path"`
|
||||
TestPath string `json:"test-path" yaml:"test-path" mapstructure:"test-path"`
|
||||
ComposeFile string `json:"compose-file" yaml:"compose-file" mapstructure:"compose-file"`
|
||||
Detach bool `json:"detach" yaml:"detach" mapstructure:"detach"`
|
||||
}
|
||||
|
||||
type InstallOptions struct {
|
||||
Namespace string `json:"namespace" yaml:"namespace" mapstructure:"namespace"`
|
||||
SpecYaml string `json:"spec-yaml" yaml:"spec-yaml" mapstructure:"spec-yaml"`
|
||||
FromYaml string `json:"from-yaml" yaml:"from-yaml" mapstructure:"from-yaml"`
|
||||
FromGoSrc string `json:"from-go-src" yaml:"from-go-src" mapstructure:"from-go-src"`
|
||||
Debug bool `json:"debug" yaml:"debug" mapstructure:"debug"`
|
||||
}
|
||||
|
||||
type BuilderVersion struct {
|
||||
Go string `json:"go" yaml:"go" mpastructure:"go"`
|
||||
TinyGo string `json:"tinygo" yaml:"tinygo" mapstructure:"tinygo"`
|
||||
Oras string `json:"oras" yaml:"oras" mapstructure:"oras"`
|
||||
}
|
||||
|
||||
type Output struct {
|
||||
Type string `json:"type" yaml:"type" mapstructure:"type"`
|
||||
Dest string `json:"dest" yaml:"dest" mapstructure:"dest"`
|
||||
}
|
||||
|
||||
// ParseOptions reads `option.yaml` and parses it into Option struct
|
||||
func ParseOptions(optionFile string, v *viper.Viper, flags *pflag.FlagSet) (*Option, error) {
|
||||
_, err := os.Stat(optionFile)
|
||||
if err != nil {
|
||||
// `option-file` is explicitly specified, but the given file does not exist
|
||||
if errors.Is(err, os.ErrNotExist) && flags.Changed("option-file") {
|
||||
return nil, errors.Errorf("option file does not exist: %q", optionFile)
|
||||
}
|
||||
} else {
|
||||
v.SetConfigFile(optionFile)
|
||||
if err = v.ReadInConfig(); err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to read option file %q", optionFile)
|
||||
}
|
||||
}
|
||||
|
||||
var opt Option
|
||||
if err = v.Unmarshal(&opt); err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to unmarshal option file %q", optionFile)
|
||||
}
|
||||
|
||||
return &opt, nil
|
||||
}
|
||||
|
||||
// AddOptionFileFlag adds `option-file` flag
|
||||
func AddOptionFileFlag(optionFile *string, flags *pflag.FlagSet) {
|
||||
flags.StringVarP(optionFile, "option-file", "f", "./option.yaml",
|
||||
"Option file for build, test and install")
|
||||
}
|
||||
89
pkg/cmd/hgctl/plugin/option/template.go
Normal file
89
pkg/cmd/hgctl/plugin/option/template.go
Normal file
@@ -0,0 +1,89 @@
|
||||
// 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 option
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
const optionYAML = `# File generated by hgctl. Modify as required.
|
||||
|
||||
version: 1.0.0
|
||||
|
||||
build:
|
||||
# The official builder image version
|
||||
builder:
|
||||
go: 1.19
|
||||
tinygo: 0.28.1
|
||||
oras: 1.0.0
|
||||
# The WASM plugin project directory
|
||||
input: ./
|
||||
# The output of the build products
|
||||
output:
|
||||
# Choose between 'files' and 'image'
|
||||
type: files
|
||||
# Destination address: when type=files, specify the local directory path, e.g., './out' or
|
||||
# type=image, specify the remote docker repository, e.g., 'docker.io/<your_username>/<your_image>'
|
||||
dest: ./out
|
||||
# The authentication configuration for pushing image to the docker repository
|
||||
docker-auth: ~/.docker/config.json
|
||||
# The directory for the WASM plugin configuration structure
|
||||
model-dir: ./
|
||||
# The WASM plugin configuration structure name
|
||||
model: PluginConfig
|
||||
# Enable debug mode
|
||||
debug: false
|
||||
|
||||
test:
|
||||
# Test environment name, that is a docker compose project name
|
||||
name: wasm-test
|
||||
# The output path to build products, that is the source of test configuration parameters
|
||||
from-path: ./out
|
||||
# The test configuration source
|
||||
test-path: ./test
|
||||
# Docker compose configuration, which is empty, looks for the following files from 'test-path':
|
||||
# compose.yaml, compose.yml, docker-compose.yml, docker-compose.yaml
|
||||
compose-file:
|
||||
# Detached mode: Run containers in the background
|
||||
detach: false
|
||||
|
||||
install:
|
||||
# The namespace of the installation
|
||||
namespace: higress-system
|
||||
# Use to validate WASM plugin configuration when install by yaml
|
||||
spec-yaml: ./out/spec.yaml
|
||||
# Installation source. Choose between 'from-yaml' and 'from-go-project'
|
||||
from-yaml: ./test/plugin-conf.yaml
|
||||
# If 'from-go-src' is non-empty, the output type of the build option must be 'image'
|
||||
from-go-src:
|
||||
# Enable debug mode
|
||||
debug: false
|
||||
`
|
||||
|
||||
func GenOptionYAML(dir string) error {
|
||||
path := fmt.Sprintf("%s/option.yaml", dir)
|
||||
f, err := os.Create(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if _, err = f.WriteString(optionYAML); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
45
pkg/cmd/hgctl/plugin/plugin.go
Normal file
45
pkg/cmd/hgctl/plugin/plugin.go
Normal file
@@ -0,0 +1,45 @@
|
||||
// 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 plugin
|
||||
|
||||
import (
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/plugin/build"
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/plugin/config"
|
||||
plugininit "github.com/alibaba/higress/pkg/cmd/hgctl/plugin/init"
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/plugin/install"
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/plugin/ls"
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/plugin/test"
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/plugin/uninstall"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func NewCommand() *cobra.Command {
|
||||
pluginCommand := &cobra.Command{
|
||||
Use: "plugin",
|
||||
Aliases: []string{"plg", "p"},
|
||||
Short: "For the Golang WASM plugin",
|
||||
}
|
||||
|
||||
pluginCommand.AddCommand(build.NewCommand())
|
||||
pluginCommand.AddCommand(install.NewCommand())
|
||||
pluginCommand.AddCommand(uninstall.NewCommand())
|
||||
pluginCommand.AddCommand(ls.NewCommand())
|
||||
pluginCommand.AddCommand(test.NewCommand())
|
||||
pluginCommand.AddCommand(config.NewCommand())
|
||||
pluginCommand.AddCommand(plugininit.NewCommand())
|
||||
|
||||
return pluginCommand
|
||||
}
|
||||
108
pkg/cmd/hgctl/plugin/test/clean.go
Normal file
108
pkg/cmd/hgctl/plugin/test/clean.go
Normal file
@@ -0,0 +1,108 @@
|
||||
// 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 test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/docker"
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/plugin/option"
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/plugin/utils"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
)
|
||||
|
||||
type cleaner struct {
|
||||
optionFile string
|
||||
option.TestOptions
|
||||
|
||||
w io.Writer
|
||||
}
|
||||
|
||||
func newCleanCommand() *cobra.Command {
|
||||
var c cleaner
|
||||
v := viper.New()
|
||||
|
||||
cleanCmd := &cobra.Command{
|
||||
Use: "clean",
|
||||
Aliases: []string{"cl"},
|
||||
Short: "Clean the test environment, that is remove the source of test configuration",
|
||||
Example: ` hgctl plugin test clean`,
|
||||
PreRun: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.CheckErr(c.config(v, cmd))
|
||||
},
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.CheckErr(c.clean())
|
||||
},
|
||||
}
|
||||
|
||||
flags := cleanCmd.PersistentFlags()
|
||||
option.AddOptionFileFlag(&c.optionFile, flags)
|
||||
v.BindPFlags(flags)
|
||||
|
||||
flags.StringP("name", "p", "wasm-test", "Test environment name")
|
||||
v.BindPFlag("test.name", flags.Lookup("name"))
|
||||
v.SetDefault("test.name", "wasm-test")
|
||||
|
||||
// TODO(WeixinX): Obtain the test configuration source directory based on the test environment name (hgctl plugin test ls)
|
||||
flags.StringP("test-path", "t", "./test", "Test configuration source")
|
||||
v.BindPFlag("test.test-path", flags.Lookup("test-path"))
|
||||
v.SetDefault("test.test-path", "./test")
|
||||
|
||||
return cleanCmd
|
||||
}
|
||||
|
||||
func (c *cleaner) config(v *viper.Viper, cmd *cobra.Command) error {
|
||||
allOpt, err := option.ParseOptions(c.optionFile, v, cmd.PersistentFlags())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.TestOptions = allOpt.Test
|
||||
|
||||
c.w = cmd.OutOrStdout()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *cleaner) clean() error {
|
||||
cli, err := docker.NewCompose(c.w)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to build the docker compose client")
|
||||
}
|
||||
|
||||
err = cli.Down(context.TODO(), c.Name)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to stop the test environment %q", c.Name)
|
||||
}
|
||||
fmt.Fprintf(c.w, "Stopped the test environment %q\n", c.Name)
|
||||
|
||||
source, err := utils.GetAbsolutePath(c.TestPath)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "invalid test configuration source %q", c.TestPath)
|
||||
}
|
||||
err = os.RemoveAll(source)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to remove the test configuration source %q", source)
|
||||
}
|
||||
fmt.Fprintf(c.w, "Removed the source %q\n", source)
|
||||
|
||||
return nil
|
||||
}
|
||||
175
pkg/cmd/hgctl/plugin/test/create.go
Normal file
175
pkg/cmd/hgctl/plugin/test/create.go
Normal file
@@ -0,0 +1,175 @@
|
||||
// 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 test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"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/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"gopkg.in/yaml.v3"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
)
|
||||
|
||||
type creator struct {
|
||||
optionFile string
|
||||
option.TestOptions
|
||||
|
||||
w io.Writer
|
||||
}
|
||||
|
||||
func newCreateCommand() *cobra.Command {
|
||||
var c creator
|
||||
v := viper.New()
|
||||
|
||||
createCmd := &cobra.Command{
|
||||
Use: "create",
|
||||
Aliases: []string{"c"},
|
||||
Short: "Create the test environment",
|
||||
Example: ` # If the option.yaml file exists in the current path, do the following:
|
||||
hgctl plugin test create
|
||||
|
||||
# Explicitly specify the source of the parameters (directory of the build
|
||||
products) and the directory where the test configuration files is stored
|
||||
hgctl plugin test create -d ./out -t ./test
|
||||
`,
|
||||
|
||||
PreRun: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.CheckErr(c.config(v, cmd))
|
||||
},
|
||||
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.CheckErr(c.create())
|
||||
},
|
||||
}
|
||||
|
||||
flags := createCmd.PersistentFlags()
|
||||
option.AddOptionFileFlag(&c.optionFile, flags)
|
||||
v.BindPFlags(flags)
|
||||
|
||||
flags.StringP("from-path", "d", "./out", "Path of storing the build products")
|
||||
v.BindPFlag("test.from-path", flags.Lookup("from-path"))
|
||||
v.SetDefault("test.from-path", "./out")
|
||||
|
||||
flags.StringP("test-path", "t", "./test", "Path for storing the test configuration")
|
||||
v.BindPFlag("test.test-path", flags.Lookup("test-path"))
|
||||
v.SetDefault("test.test-path", "./test")
|
||||
|
||||
return createCmd
|
||||
}
|
||||
|
||||
func (c *creator) config(v *viper.Viper, cmd *cobra.Command) error {
|
||||
allOpt, err := option.ParseOptions(c.optionFile, v, cmd.PersistentFlags())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.TestOptions = allOpt.Test
|
||||
|
||||
c.w = cmd.OutOrStdout()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *creator) create() (err error) {
|
||||
source, err := utils.GetAbsolutePath(c.FromPath)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "invalid build products path %q", c.FromPath)
|
||||
}
|
||||
c.FromPath = source
|
||||
|
||||
target, err := utils.GetAbsolutePath(c.TestPath)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "invalid test path %q", c.TestPath)
|
||||
}
|
||||
c.TestPath = target
|
||||
|
||||
fields := testTmplFields{}
|
||||
|
||||
// 1. extract the parameters from spec.yaml and convert them to PluginConf
|
||||
path := fmt.Sprintf("%s/spec.yaml", c.FromPath)
|
||||
spec, err := types.ParseSpecYAML(path)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to parse %s", path)
|
||||
}
|
||||
fields.PluginConf, err = config.ExtractPluginConfFrom(spec, "", "")
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to get the parameters of plugin-conf.yaml from %s", path)
|
||||
}
|
||||
|
||||
// 2. get DockerCompose instance
|
||||
fields.DockerCompose = &DockerCompose{
|
||||
TestPath: c.TestPath,
|
||||
ProductPath: c.FromPath,
|
||||
}
|
||||
|
||||
// 3. get Envoy instance
|
||||
var obj interface{}
|
||||
conf := spec.GetConfigExample()
|
||||
err = yaml.Unmarshal([]byte(conf), &obj)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to get the example of wasm plugin")
|
||||
}
|
||||
b, err := json.MarshalIndent(obj, "", strings.Repeat(" ", 2))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to marshal example to json")
|
||||
}
|
||||
jsExample := utils.AddIndent(string(b), strings.Repeat(" ", 30))
|
||||
fields.Envoy = &Envoy{JSONExample: jsExample}
|
||||
|
||||
// 4. generate corresponding test files
|
||||
if err = os.MkdirAll(target, 0755); err != nil {
|
||||
return errors.Wrap(err, "failed to create the test environment")
|
||||
|
||||
}
|
||||
if err = c.genTestConfFiles(fields); err != nil {
|
||||
return errors.Wrap(err, "failed to create the test environment")
|
||||
}
|
||||
|
||||
fmt.Fprintf(c.w, "Created the test environment in %q\n", target)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type testTmplFields struct {
|
||||
PluginConf *config.PluginConf // for plugin-conf.yaml
|
||||
DockerCompose *DockerCompose // for docker-compose.yaml
|
||||
Envoy *Envoy // for envoy.yaml
|
||||
}
|
||||
|
||||
func (c *creator) genTestConfFiles(fields testTmplFields) (err error) {
|
||||
if err = config.GenPluginConfYAML(fields.PluginConf, c.TestPath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = genDockerComposeYAML(fields.DockerCompose, c.TestPath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = genEnvoyYAML(fields.Envoy, c.TestPath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
64
pkg/cmd/hgctl/plugin/test/ls.go
Normal file
64
pkg/cmd/hgctl/plugin/test/ls.go
Normal file
@@ -0,0 +1,64 @@
|
||||
// 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 test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/docker"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/cli-runtime/pkg/printers"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
)
|
||||
|
||||
func newLsCommand() *cobra.Command {
|
||||
lsCmd := &cobra.Command{
|
||||
Use: "ls",
|
||||
Aliases: []string{"l"},
|
||||
Short: "List all test environments",
|
||||
Example: ` hgctl plugin test ls`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.CheckErr(runLs(cmd.OutOrStdout()))
|
||||
},
|
||||
}
|
||||
|
||||
return lsCmd
|
||||
}
|
||||
|
||||
func runLs(w io.Writer) error {
|
||||
cli, err := docker.NewCompose(w)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to build the docker compose client")
|
||||
}
|
||||
|
||||
list, err := cli.List(context.TODO())
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to list all test environments")
|
||||
}
|
||||
|
||||
printer := printers.GetNewTabWriter(w)
|
||||
// fmt.Fprintf(printer, "NAME\tSTATUS\tCONFIG FILES\n") // compose v2.3.0+
|
||||
fmt.Fprintf(printer, "NAME\tSTATUS\n")
|
||||
for _, stack := range list {
|
||||
fmt.Fprintf(printer, "%s\t%s\n", stack.Name, stack.Status)
|
||||
}
|
||||
printer.Flush()
|
||||
|
||||
return nil
|
||||
}
|
||||
115
pkg/cmd/hgctl/plugin/test/start.go
Normal file
115
pkg/cmd/hgctl/plugin/test/start.go
Normal file
@@ -0,0 +1,115 @@
|
||||
// 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 test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/docker"
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/plugin/option"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
)
|
||||
|
||||
// TODO(WeixinX): If no test environment exists, create one first and then start
|
||||
type starter struct {
|
||||
optionFile string
|
||||
option.TestOptions
|
||||
|
||||
w io.Writer
|
||||
}
|
||||
|
||||
func newStartCommand() *cobra.Command {
|
||||
var s starter
|
||||
v := viper.New()
|
||||
|
||||
startCmd := &cobra.Command{
|
||||
Use: "start",
|
||||
Aliases: []string{"s"},
|
||||
Short: "Start the test environment",
|
||||
Example: ` # If the option.yaml file exists in the current path, do the following:
|
||||
hgctl plugin test start
|
||||
|
||||
# Run containers in the background with the option --detach(-d)
|
||||
hgctl plugin test start -d
|
||||
`,
|
||||
PreRun: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.CheckErr(s.config(v, cmd))
|
||||
},
|
||||
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.CheckErr(s.start())
|
||||
},
|
||||
}
|
||||
|
||||
flags := startCmd.PersistentFlags()
|
||||
option.AddOptionFileFlag(&s.optionFile, flags)
|
||||
v.BindPFlags(flags)
|
||||
|
||||
flags.StringP("name", "p", "wasm-test", "Test environment name")
|
||||
v.BindPFlag("test.name", flags.Lookup("name"))
|
||||
v.SetDefault("test.name", "wasm-test")
|
||||
|
||||
flags.StringP("test-path", "t", "./test", "Test configuration source")
|
||||
v.BindPFlag("test.test-path", flags.Lookup("test-path"))
|
||||
v.SetDefault("test.test-path", "./test")
|
||||
|
||||
flags.StringP("compose-file", "c", "", "Docker compose configuration file")
|
||||
v.BindPFlag("test.compose-file", flags.Lookup("compose-file"))
|
||||
v.SetDefault("test.compose-file", "")
|
||||
|
||||
flags.BoolP("detach", "d", false, "Detached mode: Run containers in the background")
|
||||
v.BindPFlag("test.detach", flags.Lookup("detach"))
|
||||
v.SetDefault("test.detach", false)
|
||||
|
||||
return startCmd
|
||||
}
|
||||
|
||||
func (s *starter) config(v *viper.Viper, cmd *cobra.Command) error {
|
||||
allOpt, err := option.ParseOptions(s.optionFile, v, cmd.PersistentFlags())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.TestOptions = allOpt.Test
|
||||
|
||||
s.w = cmd.OutOrStdout()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *starter) start() error {
|
||||
cli, err := docker.NewCompose(s.w)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to build the docker compose client")
|
||||
}
|
||||
|
||||
var configs []string
|
||||
if s.ComposeFile != "" {
|
||||
configs = []string{s.ComposeFile}
|
||||
}
|
||||
|
||||
err = cli.Up(context.TODO(), s.Name, configs, s.TestPath, s.Detach)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to start the test environment")
|
||||
}
|
||||
fmt.Fprintf(s.w, "Started the test environment %q\n", s.Name)
|
||||
|
||||
return nil
|
||||
}
|
||||
95
pkg/cmd/hgctl/plugin/test/stop.go
Normal file
95
pkg/cmd/hgctl/plugin/test/stop.go
Normal file
@@ -0,0 +1,95 @@
|
||||
// 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 test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/docker"
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/plugin/option"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
)
|
||||
|
||||
type stopper struct {
|
||||
optionFile string
|
||||
option.TestOptions
|
||||
|
||||
w io.Writer
|
||||
}
|
||||
|
||||
func newStopCommand() *cobra.Command {
|
||||
var s stopper
|
||||
v := viper.New()
|
||||
|
||||
stopCmd := &cobra.Command{
|
||||
Use: "stop",
|
||||
Aliases: []string{"st"},
|
||||
Short: "Stop the test environment",
|
||||
Example: ` # Stop responding to the compose containers with the option --name(-p)
|
||||
hgctl plugin test stop -p wasm-test
|
||||
`,
|
||||
|
||||
PreRun: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.CheckErr(s.config(v, cmd))
|
||||
},
|
||||
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.CheckErr(s.stop())
|
||||
},
|
||||
}
|
||||
|
||||
flags := stopCmd.PersistentFlags()
|
||||
option.AddOptionFileFlag(&s.optionFile, flags)
|
||||
v.BindPFlags(flags)
|
||||
|
||||
flags.StringP("name", "p", "wasm-test", "Test environment name")
|
||||
v.BindPFlag("test.name", flags.Lookup("name"))
|
||||
v.SetDefault("test.name", "wasm-test")
|
||||
|
||||
return stopCmd
|
||||
}
|
||||
|
||||
func (s *stopper) config(v *viper.Viper, cmd *cobra.Command) error {
|
||||
allOpt, err := option.ParseOptions(s.optionFile, v, cmd.PersistentFlags())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.TestOptions = allOpt.Test
|
||||
|
||||
s.w = cmd.OutOrStdout()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *stopper) stop() error {
|
||||
cli, err := docker.NewCompose(s.w)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to build the docker compose client")
|
||||
}
|
||||
|
||||
err = cli.Down(context.TODO(), s.Name)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to stop the test environment %q", s.Name)
|
||||
}
|
||||
fmt.Fprintf(s.w, "Stopped the test environment %q\n", s.Name)
|
||||
|
||||
return nil
|
||||
}
|
||||
167
pkg/cmd/hgctl/plugin/test/templates.go
Normal file
167
pkg/cmd/hgctl/plugin/test/templates.go
Normal file
@@ -0,0 +1,167 @@
|
||||
// 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 test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
const (
|
||||
dockerComposeYAML = `# File generated by hgctl. Modify as required.
|
||||
|
||||
version: '3.7'
|
||||
services:
|
||||
envoy:
|
||||
image: higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/envoy:1.20
|
||||
command: envoy -c /etc/envoy/envoy.yaml --component-log-level wasm:debug
|
||||
depends_on:
|
||||
- httpbin
|
||||
networks:
|
||||
- wasmtest
|
||||
ports:
|
||||
- "10000:10000"
|
||||
volumes:
|
||||
- {{ .TestPath }}/envoy.yaml:/etc/envoy/envoy.yaml
|
||||
- {{ .ProductPath }}/plugin.wasm:/etc/envoy/plugin.wasm
|
||||
|
||||
httpbin:
|
||||
image: kennethreitz/httpbin:latest
|
||||
networks:
|
||||
- wasmtest
|
||||
ports:
|
||||
- "12345:80"
|
||||
|
||||
networks:
|
||||
wasmtest: {}
|
||||
`
|
||||
|
||||
envoyYAML = `# File generated by hgctl. Modify as required.
|
||||
|
||||
admin:
|
||||
address:
|
||||
socket_address:
|
||||
protocol: TCP
|
||||
address: 0.0.0.0
|
||||
port_value: 9901
|
||||
static_resources:
|
||||
listeners:
|
||||
- name: listener_0
|
||||
address:
|
||||
socket_address:
|
||||
protocol: TCP
|
||||
address: 0.0.0.0
|
||||
port_value: 10000
|
||||
filter_chains:
|
||||
- filters:
|
||||
- name: envoy.filters.network.http_connection_manager
|
||||
typed_config:
|
||||
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
|
||||
scheme_header_transformation:
|
||||
scheme_to_overwrite: https
|
||||
stat_prefix: ingress_http
|
||||
# Output envoy logs to stdout
|
||||
access_log:
|
||||
- name: envoy.access_loggers.stdout
|
||||
typed_config:
|
||||
"@type": type.googleapis.com/envoy.extensions.access_loggers.stream.v3.StdoutAccessLog
|
||||
# Modify as required
|
||||
route_config:
|
||||
name: local_route
|
||||
virtual_hosts:
|
||||
- name: local_service
|
||||
domains: ["*"]
|
||||
routes:
|
||||
- match:
|
||||
prefix: "/"
|
||||
route:
|
||||
cluster: httpbin
|
||||
http_filters:
|
||||
- name: wasmtest
|
||||
typed_config:
|
||||
"@type": type.googleapis.com/udpa.type.v1.TypedStruct
|
||||
type_url: type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm
|
||||
value:
|
||||
config:
|
||||
name: wasmtest
|
||||
vm_config:
|
||||
runtime: envoy.wasm.runtime.v8
|
||||
code:
|
||||
local:
|
||||
filename: /etc/envoy/plugin.wasm
|
||||
configuration:
|
||||
"@type": "type.googleapis.com/google.protobuf.StringValue"
|
||||
# Modify as required
|
||||
value: |
|
||||
{{ .JSONExample }}
|
||||
- name: envoy.filters.http.router
|
||||
clusters:
|
||||
- name: httpbin
|
||||
connect_timeout: 30s
|
||||
type: LOGICAL_DNS
|
||||
# Comment out the following line to test on v6 networks
|
||||
dns_lookup_family: V4_ONLY
|
||||
lb_policy: ROUND_ROBIN
|
||||
load_assignment:
|
||||
cluster_name: httpbin
|
||||
endpoints:
|
||||
- lb_endpoints:
|
||||
- endpoint:
|
||||
address:
|
||||
socket_address:
|
||||
address: httpbin
|
||||
port_value: 80
|
||||
`
|
||||
)
|
||||
|
||||
type DockerCompose struct {
|
||||
TestPath string
|
||||
ProductPath string
|
||||
}
|
||||
|
||||
type Envoy struct {
|
||||
JSONExample string
|
||||
}
|
||||
|
||||
func genDockerComposeYAML(d *DockerCompose, dir string) error {
|
||||
path := fmt.Sprintf("%s/docker-compose.yaml", dir)
|
||||
f, err := os.Create(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if err = template.Must(template.New("DockerComposeYAML").Parse(dockerComposeYAML)).Execute(f, d); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func genEnvoyYAML(e *Envoy, dir string) error {
|
||||
path := fmt.Sprintf("%s/envoy.yaml", dir)
|
||||
f, err := os.Create(path)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("failed to create %q: %v\n", path, err))
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if err = template.Must(template.New("EnvoyYAML").Parse(envoyYAML)).Execute(f, e); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
35
pkg/cmd/hgctl/plugin/test/test.go
Normal file
35
pkg/cmd/hgctl/plugin/test/test.go
Normal file
@@ -0,0 +1,35 @@
|
||||
// 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 test
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func NewCommand() *cobra.Command {
|
||||
testCmd := &cobra.Command{
|
||||
Use: "test",
|
||||
Aliases: []string{"t"},
|
||||
Short: "Test WASM plugin locally",
|
||||
}
|
||||
|
||||
testCmd.AddCommand(newCreateCommand())
|
||||
testCmd.AddCommand(newStartCommand())
|
||||
testCmd.AddCommand(newStopCommand())
|
||||
testCmd.AddCommand(newCleanCommand())
|
||||
testCmd.AddCommand(newLsCommand())
|
||||
|
||||
return testCmd
|
||||
}
|
||||
163
pkg/cmd/hgctl/plugin/types/annotation.go
Normal file
163
pkg/cmd/hgctl/plugin/types/annotation.go
Normal file
@@ -0,0 +1,163 @@
|
||||
// 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 types
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type Annotation struct {
|
||||
Type AnnotationType
|
||||
I18nType I18nType
|
||||
Text string
|
||||
}
|
||||
|
||||
type AnnotationType int
|
||||
|
||||
const (
|
||||
// Info
|
||||
ACategory AnnotationType = iota
|
||||
AName
|
||||
ATitle
|
||||
ADescription
|
||||
AIconUrl
|
||||
AVersion
|
||||
AContactName
|
||||
AContactUrl
|
||||
AContactEmail
|
||||
|
||||
// Spec
|
||||
APhase
|
||||
APriority
|
||||
|
||||
// Schema
|
||||
AScope
|
||||
AExample
|
||||
AEnd
|
||||
|
||||
AUnknown
|
||||
)
|
||||
|
||||
func str2AnnotationType(typ string) AnnotationType {
|
||||
switch strings.ToLower(typ) {
|
||||
case "@category":
|
||||
return ACategory
|
||||
case "@name":
|
||||
return AName
|
||||
case "@title":
|
||||
return ATitle
|
||||
case "@description":
|
||||
return ADescription
|
||||
case "@iconurl":
|
||||
return AIconUrl
|
||||
case "@version":
|
||||
return AVersion
|
||||
case "@contact.name":
|
||||
return AContactName
|
||||
case "@contact.url":
|
||||
return AContactUrl
|
||||
case "@contact.email":
|
||||
return AContactEmail
|
||||
case "@phase":
|
||||
return APhase
|
||||
case "@priority":
|
||||
return APriority
|
||||
case "@scope":
|
||||
return AScope
|
||||
case "@example":
|
||||
return AExample
|
||||
case "@end":
|
||||
return AEnd
|
||||
default:
|
||||
return AUnknown
|
||||
}
|
||||
}
|
||||
|
||||
// GetAnnotations returns all annotations in the comment
|
||||
func GetAnnotations(comment string) []Annotation {
|
||||
as := make([]Annotation, 0)
|
||||
cs := strings.Split(comment, "\n")
|
||||
for i := 0; i < len(cs); i++ {
|
||||
a, err := getAnnotationFrom(cs[i])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if a.Type == AExample {
|
||||
for j := i + 1; j < len(cs); j++ {
|
||||
if str2AnnotationType(strings.TrimSpace(cs[j])) == AEnd {
|
||||
break
|
||||
}
|
||||
if j == i+1 {
|
||||
a.Text = fmt.Sprintf("%s", cs[j])
|
||||
} else {
|
||||
a.Text = fmt.Sprintf("%s\n%s", a.Text, cs[j])
|
||||
}
|
||||
}
|
||||
}
|
||||
as = append(as, a)
|
||||
}
|
||||
return as
|
||||
}
|
||||
|
||||
func getAnnotationFrom(c string) (Annotation, error) {
|
||||
// the annotation is like `@AnnotationType [I18nType] Text`
|
||||
|
||||
c = strings.TrimSpace(c)
|
||||
if !strings.HasPrefix(c, "@") {
|
||||
return Annotation{}, errors.New("invalid annotation")
|
||||
}
|
||||
|
||||
// first param: AnnotationType
|
||||
idx := strings.Index(c, " ")
|
||||
if idx == -1 && str2AnnotationType(c) == AUnknown { // only an invalid annotation type
|
||||
return Annotation{}, errors.New("invalid annotation")
|
||||
}
|
||||
// idx != -1 or type != unknown
|
||||
var typ AnnotationType
|
||||
if idx == -1 {
|
||||
typ = str2AnnotationType(c)
|
||||
} else {
|
||||
typ = str2AnnotationType(strings.TrimSpace(c[0:idx]))
|
||||
}
|
||||
c = strings.TrimSpace(c[idx+1:])
|
||||
a := Annotation{
|
||||
Type: typ,
|
||||
I18nType: I18nDefault,
|
||||
Text: c,
|
||||
}
|
||||
if a.Type != ATitle && a.Type != ADescription { // other annotation types do not define i18n
|
||||
a.I18nType = I18nUndefined
|
||||
}
|
||||
if idx == -1 && typ != AUnknown { // only a valid annotation type
|
||||
a.Text = ""
|
||||
}
|
||||
|
||||
// second or/and third param: I18nType and Text
|
||||
idx = strings.Index(c, " ")
|
||||
if idx == -1 {
|
||||
return a, nil
|
||||
}
|
||||
i18n := str2I18nType(strings.TrimSpace(c[0:idx]))
|
||||
if i18n == I18nUnknown {
|
||||
return a, nil
|
||||
}
|
||||
a.I18nType = i18n
|
||||
a.Text = strings.TrimSpace(c[idx+1:])
|
||||
return a, nil
|
||||
}
|
||||
176
pkg/cmd/hgctl/plugin/types/marshal.go
Normal file
176
pkg/cmd/hgctl/plugin/types/marshal.go
Normal file
@@ -0,0 +1,176 @@
|
||||
// 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 types
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"gopkg.in/yaml.v3"
|
||||
"k8s.io/apimachinery/pkg/util/json"
|
||||
)
|
||||
|
||||
func (s JSON) MarshalJSON() ([]byte, error) {
|
||||
if len(s.Raw) > 0 {
|
||||
var obj interface{}
|
||||
err := json.Unmarshal(s.Raw, &obj)
|
||||
if err != nil {
|
||||
return []byte("null"), err
|
||||
}
|
||||
return json.Marshal(obj)
|
||||
}
|
||||
return []byte("null"), nil
|
||||
}
|
||||
|
||||
func (s *JSON) UnmarshalJSON(data []byte) error {
|
||||
if len(data) > 0 && !bytes.Equal(data, []byte("null")) {
|
||||
s.Raw = data
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s JSON) MarshalYAML() (interface{}, error) {
|
||||
if len(s.Raw) > 0 {
|
||||
var obj interface{}
|
||||
err := yaml.Unmarshal(s.Raw, &obj)
|
||||
if err != nil {
|
||||
return "null", err
|
||||
}
|
||||
return obj, nil
|
||||
}
|
||||
return "null", nil
|
||||
}
|
||||
|
||||
func (s JSONSchemaPropsOrArray) MarshalJSON() ([]byte, error) {
|
||||
if len(s.JSONSchemas) > 0 {
|
||||
return json.Marshal(s.JSONSchemas)
|
||||
}
|
||||
return json.Marshal(s.Schema)
|
||||
}
|
||||
|
||||
func (s *JSONSchemaPropsOrArray) UnmarshalJSON(data []byte) error {
|
||||
var nw JSONSchemaPropsOrArray
|
||||
var first byte
|
||||
if len(data) > 1 {
|
||||
first = data[0]
|
||||
}
|
||||
if first == '{' {
|
||||
var sch JSONSchemaProps
|
||||
if err := json.Unmarshal(data, &sch); err != nil {
|
||||
return err
|
||||
}
|
||||
nw.Schema = &sch
|
||||
}
|
||||
if first == '[' {
|
||||
if err := json.Unmarshal(data, &nw.JSONSchemas); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
*s = nw
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s JSONSchemaPropsOrArray) MarshalYAML() (interface{}, error) {
|
||||
if len(s.JSONSchemas) > 0 {
|
||||
return s.JSONSchemas, nil
|
||||
}
|
||||
return s.Schema, nil
|
||||
}
|
||||
|
||||
func (s JSONSchemaPropsOrBool) MarshalJSON() ([]byte, error) {
|
||||
if s.Schema != nil {
|
||||
return json.Marshal(s.Schema)
|
||||
}
|
||||
|
||||
if s.Schema == nil && !s.Allows {
|
||||
return []byte("false"), nil
|
||||
}
|
||||
return []byte("true"), nil
|
||||
}
|
||||
|
||||
func (s *JSONSchemaPropsOrBool) UnmarshalJSON(data []byte) error {
|
||||
var nw JSONSchemaPropsOrBool
|
||||
switch {
|
||||
case len(data) == 0:
|
||||
case data[0] == '{':
|
||||
var sch JSONSchemaProps
|
||||
if err := json.Unmarshal(data, &sch); err != nil {
|
||||
return err
|
||||
}
|
||||
nw.Allows = true
|
||||
nw.Schema = &sch
|
||||
case len(data) == 4 && string(data) == "true":
|
||||
nw.Allows = true
|
||||
case len(data) == 5 && string(data) == "false":
|
||||
nw.Allows = false
|
||||
default:
|
||||
return errors.New("boolean or JSON schema expected")
|
||||
}
|
||||
*s = nw
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s JSONSchemaPropsOrBool) MarshalYAML() (interface{}, error) {
|
||||
if s.Schema != nil {
|
||||
return yaml.Marshal(s.Schema)
|
||||
}
|
||||
|
||||
if s.Schema == nil && !s.Allows {
|
||||
return false, nil
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (s JSONSchemaPropsOrStringArray) MarshalJSON() ([]byte, error) {
|
||||
if len(s.Property) > 0 {
|
||||
return json.Marshal(s.Property)
|
||||
}
|
||||
if s.Schema != nil {
|
||||
return json.Marshal(s.Schema)
|
||||
}
|
||||
return []byte("null"), nil
|
||||
}
|
||||
|
||||
func (s *JSONSchemaPropsOrStringArray) UnmarshalJSON(data []byte) error {
|
||||
var first byte
|
||||
if len(data) > 1 {
|
||||
first = data[0]
|
||||
}
|
||||
var nw JSONSchemaPropsOrStringArray
|
||||
if first == '{' {
|
||||
var sch JSONSchemaProps
|
||||
if err := json.Unmarshal(data, &sch); err != nil {
|
||||
return err
|
||||
}
|
||||
nw.Schema = &sch
|
||||
}
|
||||
if first == '[' {
|
||||
if err := json.Unmarshal(data, &nw.Property); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
*s = nw
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s JSONSchemaPropsOrStringArray) MarshalYAML() (interface{}, error) {
|
||||
if len(s.Property) > 0 {
|
||||
return s.Property, nil
|
||||
}
|
||||
if s.Schema != nil {
|
||||
return s.Schema, nil
|
||||
}
|
||||
return "null", nil
|
||||
}
|
||||
393
pkg/cmd/hgctl/plugin/types/meta.go
Normal file
393
pkg/cmd/hgctl/plugin/types/meta.go
Normal file
@@ -0,0 +1,393 @@
|
||||
// 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 types
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/iancoleman/orderedmap"
|
||||
k8syaml "k8s.io/apimachinery/pkg/util/yaml"
|
||||
)
|
||||
|
||||
// WasmPluginMeta is used to describe WASM plugin metadata,
|
||||
// see https://higress.io/en-us/docs/user/wasm-image-spec/
|
||||
type WasmPluginMeta struct {
|
||||
APIVersion string `json:"apiVersion" yaml:"apiVersion"`
|
||||
Info WasmPluginInfo `json:"info" yaml:"info"`
|
||||
Spec WasmPluginSpec `json:"spec" yaml:"spec"`
|
||||
}
|
||||
|
||||
func defaultWsamPluginMeta() *WasmPluginMeta {
|
||||
return &WasmPluginMeta{
|
||||
APIVersion: "1.0.0",
|
||||
Info: WasmPluginInfo{
|
||||
Category: CategoryCustom,
|
||||
Name: "Unnamed",
|
||||
XTitleI18n: make(map[I18nType]string),
|
||||
XDescriptionI18n: make(map[I18nType]string),
|
||||
Version: "0.1.0",
|
||||
},
|
||||
Spec: WasmPluginSpec{
|
||||
Phase: PhaseUnspecified,
|
||||
Priority: 0,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ParseSpecYAML parses the `spec.yaml` to WasmPluginMeta
|
||||
func ParseSpecYAML(spec string) (*WasmPluginMeta, error) {
|
||||
f, err := os.Open(spec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
var m WasmPluginMeta
|
||||
dc := k8syaml.NewYAMLOrJSONDecoder(f, 4096)
|
||||
if err = dc.Decode(&m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &m, nil
|
||||
}
|
||||
|
||||
// ParseGoSrc parses the config model of the golang WASM plugin project to WasmPluginMeta
|
||||
func ParseGoSrc(dir, model string) (*WasmPluginMeta, error) {
|
||||
mp, err := NewModelParser(dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m, err := mp.GetModel(model)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
meta := defaultWsamPluginMeta()
|
||||
meta.setByConfigModel(m)
|
||||
return meta, nil
|
||||
}
|
||||
|
||||
func (meta *WasmPluginMeta) setByConfigModel(model *Model) {
|
||||
_, schema := recursiveSetSchema(model, nil)
|
||||
meta.Spec.ConfigSchema.OpenAPIV3Schema = schema
|
||||
meta.setModelAnnotations(model.Doc)
|
||||
}
|
||||
|
||||
func recursiveSetSchema(model *Model, parent *JSONSchemaProps) (string, *JSONSchemaProps) {
|
||||
cur := NewJSONSchemaProps()
|
||||
cur.Type = model.Type
|
||||
if parent != nil {
|
||||
cur.HandleFieldAnnotations(model.Doc)
|
||||
}
|
||||
newName := cur.HandleFieldTags(model.Tag, parent, model.Name)
|
||||
if IsArray(model.Type) {
|
||||
item := NewJSONSchemaProps()
|
||||
item.Type = GetItemType(cur.Type)
|
||||
cur.Type = "array"
|
||||
if IsObject(item.Type) {
|
||||
item.Properties = make(map[string]JSONSchemaProps)
|
||||
for _, field := range model.Fields {
|
||||
name, child := recursiveSetSchema(&field, cur)
|
||||
item.Properties[name] = *child
|
||||
}
|
||||
}
|
||||
cur.Items = &JSONSchemaPropsOrArray{Schema: item}
|
||||
} else if IsObject(model.Type) { // type may be `array of object`, and it is handled in the first branch
|
||||
for _, field := range model.Fields {
|
||||
name, child := recursiveSetSchema(&field, cur)
|
||||
cur.Properties[name] = *child
|
||||
}
|
||||
}
|
||||
return newName, cur
|
||||
}
|
||||
|
||||
func (meta *WasmPluginMeta) setModelAnnotations(comment string) {
|
||||
as := GetAnnotations(comment)
|
||||
for _, a := range as {
|
||||
switch a.Type {
|
||||
// Info
|
||||
case ACategory:
|
||||
meta.Info.Category = Category(a.Text)
|
||||
case AName:
|
||||
meta.Info.Name = a.Text
|
||||
case ATitle:
|
||||
if meta.Info.Title == "" {
|
||||
meta.Info.Title = a.Text
|
||||
}
|
||||
meta.Info.XTitleI18n[a.I18nType] = a.Text
|
||||
case ADescription:
|
||||
if meta.Info.Description == "" {
|
||||
meta.Info.Description = a.Text
|
||||
}
|
||||
meta.Info.XDescriptionI18n[a.I18nType] = a.Text
|
||||
case AIconUrl:
|
||||
meta.Info.IconUrl = a.Text
|
||||
case AVersion:
|
||||
meta.Info.Version = a.Text
|
||||
case AContactName:
|
||||
meta.Info.Contact.Name = a.Text
|
||||
case AContactUrl:
|
||||
meta.Info.Contact.Url = a.Text
|
||||
case AContactEmail:
|
||||
meta.Info.Contact.Email = a.Text
|
||||
|
||||
// Spec
|
||||
case APhase:
|
||||
meta.Spec.Phase = Phase(a.Text)
|
||||
case APriority:
|
||||
priority, err := strconv.ParseInt(a.Text, 10, 64)
|
||||
if err != nil {
|
||||
priority = 0
|
||||
}
|
||||
meta.Spec.Priority = priority
|
||||
|
||||
// Schema
|
||||
case AExample:
|
||||
meta.Spec.ConfigSchema.OpenAPIV3Schema.Example = &JSON{Raw: []byte(a.Text)}
|
||||
case AScope:
|
||||
meta.Spec.ConfigSchema.OpenAPIV3Schema.Scope = Scope(a.Text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type WasmPluginInfo struct {
|
||||
Category Category `json:"category" yaml:"category"`
|
||||
Name string `json:"name" yaml:"name"`
|
||||
Title string `json:"title,omitempty" yaml:"title,omitempty"`
|
||||
XTitleI18n map[I18nType]string `json:"x-title-i18n,omitempty" yaml:"x-title-i18n,omitempty"`
|
||||
Description string `json:"description,omitempty" yaml:"description,omitempty"`
|
||||
XDescriptionI18n map[I18nType]string `json:"x-description-i18n,omitempty" yaml:"x-description-i18n,omitempty"`
|
||||
IconUrl string `json:"iconUrl,omitempty" yaml:"iconUrl,omitempty"`
|
||||
Version string `json:"version" yaml:"version"`
|
||||
Contact Contact `json:"contact,omitempty" yaml:"contact,omitempty"`
|
||||
}
|
||||
|
||||
type Category string
|
||||
|
||||
const (
|
||||
CategoryAuth Category = "auth"
|
||||
CategorySecurity Category = "security"
|
||||
CategoryProtocol Category = "protocol"
|
||||
CategoryFlowControl Category = "flow-control"
|
||||
CategoryFlowMonitor Category = "flow-monitor"
|
||||
CategoryCustom Category = "custom"
|
||||
CategoryDefault = CategoryCustom
|
||||
)
|
||||
|
||||
const (
|
||||
IconAuth = "https://img.alicdn.com/imgextra/i4/O1CN01BPFGlT1pGZ2VDLgaH_!!6000000005333-2-tps-42-42.png"
|
||||
IconSecurity = "https://img.alicdn.com/imgextra/i1/O1CN01jKT9vC1O059vNaq5u_!!6000000001642-2-tps-42-42.png"
|
||||
IconProtocol = "https://img.alicdn.com/imgextra/i2/O1CN01xIywow1mVGuRUjbhe_!!6000000004959-2-tps-42-42.png"
|
||||
IconFlowControl = "https://img.alicdn.com/imgextra/i3/O1CN01bAFa9k1t1gdQcVTH0_!!6000000005842-2-tps-42-42.png"
|
||||
IconFlowMonitor = "https://img.alicdn.com/imgextra/i4/O1CN01aet3s61MoLOEEhRIo_!!6000000001481-2-tps-42-42.png"
|
||||
IconCustom = "https://img.alicdn.com/imgextra/i1/O1CN018iKKih1iVx287RltL_!!6000000004419-2-tps-42-42.png"
|
||||
IconDefault = IconCustom
|
||||
)
|
||||
|
||||
func Category2IconUrl(category Category) string {
|
||||
switch category {
|
||||
case CategoryAuth:
|
||||
return IconAuth
|
||||
case CategorySecurity:
|
||||
return IconSecurity
|
||||
case CategoryProtocol:
|
||||
return IconProtocol
|
||||
case CategoryFlowControl:
|
||||
return IconFlowControl
|
||||
case CategoryFlowMonitor:
|
||||
return IconFlowMonitor
|
||||
case CategoryCustom:
|
||||
return IconCustom
|
||||
default:
|
||||
return IconDefault
|
||||
}
|
||||
}
|
||||
|
||||
type I18nType string
|
||||
|
||||
const (
|
||||
I18nZH_CN I18nType = "zh-CN" // default
|
||||
I18nEN_US I18nType = "en-US"
|
||||
I18nUndefined I18nType = "undefined" // i18n type is empty in the annotation
|
||||
I18nUnknown I18nType = "unknown"
|
||||
I18nDefault = I18nEN_US
|
||||
)
|
||||
|
||||
func str2I18nType(typ string) I18nType {
|
||||
switch strings.ToLower(typ) {
|
||||
case "zh-cn":
|
||||
return I18nZH_CN
|
||||
case "en-us":
|
||||
return I18nEN_US
|
||||
default:
|
||||
return I18nUnknown
|
||||
}
|
||||
}
|
||||
|
||||
type Contact struct {
|
||||
Name string `json:"name,omitempty" yaml:"name,omitempty"`
|
||||
Url string `json:"url,omitempty" yaml:"url,omitempty"`
|
||||
Email string `json:"email,omitempty" yaml:"email,omitempty"`
|
||||
}
|
||||
|
||||
type WasmPluginSpec struct {
|
||||
// Phase refers to https://istio.io/latest/docs/reference/config/proxy_extensions/wasm-plugin/#PluginPhase
|
||||
Phase Phase `json:"phase" yaml:"phase"`
|
||||
|
||||
// Priority refers to https://istio.io/latest/docs/reference/config/proxy_extensions/wasm-plugin/#WasmPlugin
|
||||
Priority int64 `json:"priority" yaml:"priority"`
|
||||
|
||||
ConfigSchema ConfigSchema `json:"configSchema" yaml:"configSchema"`
|
||||
}
|
||||
|
||||
type Phase string
|
||||
|
||||
const (
|
||||
PhaseUnspecified Phase = "UNSPECIFIED_PHASE"
|
||||
PhaseAuthn Phase = "AUTHN"
|
||||
PhaseAuthz Phase = "AUTHZ"
|
||||
PhaseStats Phase = "STATS"
|
||||
PhaseDefault = PhaseUnspecified
|
||||
)
|
||||
|
||||
type ConfigSchema struct {
|
||||
OpenAPIV3Schema *JSONSchemaProps `json:"openAPIV3Schema" yaml:"openAPIV3Schema"`
|
||||
}
|
||||
|
||||
// GetConfigExample returns a pretty WASM plugin config example
|
||||
func (meta *WasmPluginMeta) GetConfigExample() string {
|
||||
s := meta.Spec.ConfigSchema.OpenAPIV3Schema
|
||||
if s != nil {
|
||||
return s.GetExample()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// getLanguageUnionOrderMap returns a ordered map of language union of title and description.
|
||||
// If there is a language type in title that description does not have, the value is "No description"
|
||||
func (meta *WasmPluginMeta) getLanguageUnionOrderMap() *orderedmap.OrderedMap {
|
||||
m := orderedmap.New()
|
||||
for i18n, desc := range meta.Info.XDescriptionI18n {
|
||||
m.Set(string(i18n), desc)
|
||||
}
|
||||
for i18n := range meta.Info.XTitleI18n {
|
||||
if _, ok := m.Get(string(i18n)); !ok {
|
||||
m.Set(string(i18n), "No description")
|
||||
}
|
||||
}
|
||||
if len(m.Keys()) == 0 {
|
||||
m.Set(string(I18nEN_US), "No description")
|
||||
}
|
||||
m.SortKeys(sort.Strings)
|
||||
return m
|
||||
}
|
||||
|
||||
// WasmUsage is used to describe WASM plugin usage in the Markdown document
|
||||
type WasmUsage struct {
|
||||
I18nType I18nType
|
||||
Description string
|
||||
ConfigEntries []ConfigEntry
|
||||
Example string
|
||||
}
|
||||
|
||||
type ConfigEntry struct {
|
||||
Name string
|
||||
Type string
|
||||
Requirement string
|
||||
Default string
|
||||
Description string
|
||||
}
|
||||
|
||||
// GetUsages returns WASM plugin usages in different languages
|
||||
func (meta *WasmPluginMeta) GetUsages() ([]WasmUsage, error) {
|
||||
usages := make([]WasmUsage, 0)
|
||||
example := meta.GetConfigExample()
|
||||
m := meta.getLanguageUnionOrderMap()
|
||||
for _, i18n := range m.Keys() {
|
||||
desc, ok := m.Get(i18n)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
u := WasmUsage{
|
||||
I18nType: I18nType(i18n),
|
||||
Description: desc.(string),
|
||||
ConfigEntries: make([]ConfigEntry, 0),
|
||||
Example: example,
|
||||
}
|
||||
getConfigEntries(meta.Spec.ConfigSchema.OpenAPIV3Schema, &u.ConfigEntries, I18nType(i18n))
|
||||
usages = append(usages, u)
|
||||
}
|
||||
|
||||
return usages, nil
|
||||
}
|
||||
|
||||
func getConfigEntries(schema *JSONSchemaProps, entries *[]ConfigEntry, i18n I18nType) {
|
||||
doGetConfigEntries(schema, entries, "", "", i18n, false)
|
||||
}
|
||||
|
||||
func doGetConfigEntries(schema *JSONSchemaProps, entries *[]ConfigEntry, parentName, name string, i18n I18nType, required bool) {
|
||||
newName := constructName(parentName, name)
|
||||
switch schema.Type {
|
||||
case "object":
|
||||
m := schema.GetPropertiesOrderMap()
|
||||
for _, fieldName := range m.Keys() {
|
||||
val, ok := m.Get(fieldName)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
props := val.(JSONSchemaProps)
|
||||
required = schema.IsRequired(fieldName)
|
||||
doGetConfigEntries(&props, entries, newName, fieldName, i18n, required)
|
||||
}
|
||||
case "array":
|
||||
itemType := schema.Items.Schema.Type
|
||||
e := ConfigEntry{
|
||||
Name: newName,
|
||||
Type: ArrayPrefix + itemType,
|
||||
Requirement: schema.JoinRequirementsBy(i18n, required),
|
||||
Default: schema.GetDefaultValue(),
|
||||
Description: schema.XDescriptionI18n[i18n],
|
||||
}
|
||||
*entries = append(*entries, e)
|
||||
if itemType == "object" {
|
||||
doGetConfigEntries(schema.Items.Schema, entries, newName+"[*]", "", i18n, false)
|
||||
}
|
||||
default:
|
||||
e := ConfigEntry{
|
||||
Name: newName,
|
||||
Type: schema.Type,
|
||||
Requirement: schema.JoinRequirementsBy(i18n, required),
|
||||
Default: schema.GetDefaultValue(),
|
||||
Description: schema.XDescriptionI18n[i18n],
|
||||
}
|
||||
*entries = append(*entries, e)
|
||||
}
|
||||
}
|
||||
|
||||
func constructName(parent, name string) string {
|
||||
newName := name
|
||||
if parent != "" {
|
||||
if name != "" {
|
||||
newName = fmt.Sprintf("%s.%s", parent, name)
|
||||
} else {
|
||||
newName = parent
|
||||
}
|
||||
}
|
||||
return newName
|
||||
}
|
||||
391
pkg/cmd/hgctl/plugin/types/model_parser.go
Normal file
391
pkg/cmd/hgctl/plugin/types/model_parser.go
Normal file
@@ -0,0 +1,391 @@
|
||||
// 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 types
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/structtag"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
ArrayPrefix = "array of "
|
||||
ObjectSuffix = "object"
|
||||
)
|
||||
|
||||
// IsArray returns true if the given type is an `array of <type>`
|
||||
func IsArray(typ string) bool {
|
||||
return strings.HasPrefix(typ, ArrayPrefix)
|
||||
}
|
||||
|
||||
// GetItemType returns the item type of array, e.g.: array of int -> int
|
||||
func GetItemType(typ string) string {
|
||||
return strings.TrimPrefix(typ, ArrayPrefix)
|
||||
}
|
||||
|
||||
// IsObject returns true if the given type is an `object` or an `array of object`
|
||||
func IsObject(typ string) bool {
|
||||
return strings.HasSuffix(typ, ObjectSuffix)
|
||||
}
|
||||
|
||||
var (
|
||||
ErrInvalidModel = errors.New("invalid model")
|
||||
ErrInvalidFiledType = errors.New("invalid field type")
|
||||
)
|
||||
|
||||
type ModelParser struct {
|
||||
structs map[string]*astNode
|
||||
|
||||
// alias for a basic type, such as type MyInt int: MyInt -> int
|
||||
// TODO(WeixinX): Support alias for package name
|
||||
alias map[string]*astNode
|
||||
}
|
||||
|
||||
type Model struct {
|
||||
Name string
|
||||
Type string
|
||||
Doc string
|
||||
Tag string
|
||||
Fields []Model
|
||||
}
|
||||
|
||||
type astNode struct {
|
||||
name string
|
||||
doc string
|
||||
expr ast.Expr
|
||||
}
|
||||
|
||||
func (m *Model) Inspect(f func(model *Model) bool) {
|
||||
ctn := f(m)
|
||||
if !ctn {
|
||||
return
|
||||
}
|
||||
|
||||
for _, field := range m.Fields {
|
||||
field.Inspect(f)
|
||||
}
|
||||
}
|
||||
|
||||
// NewModelParser new a model parser based on the dir where the given model exists
|
||||
func NewModelParser(dir string) (*ModelParser, error) {
|
||||
pkgs, err := walkGoSrc(dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p := &ModelParser{
|
||||
structs: make(map[string]*astNode),
|
||||
alias: make(map[string]*astNode),
|
||||
}
|
||||
for _, pkg := range pkgs {
|
||||
for _, f := range pkg.Files {
|
||||
for _, decl := range f.Decls {
|
||||
x, ok := decl.(*ast.GenDecl)
|
||||
if !ok || x.Tok != token.TYPE {
|
||||
continue
|
||||
}
|
||||
for _, spec := range x.Specs {
|
||||
ts, ok := spec.(*ast.TypeSpec)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
switch t := ts.Type.(type) {
|
||||
case *ast.StructType:
|
||||
if !t.Struct.IsValid() {
|
||||
continue
|
||||
}
|
||||
s := &astNode{
|
||||
name: ts.Name.String(),
|
||||
expr: t,
|
||||
}
|
||||
if pkg.Name != "main" { // ignore main package prefix
|
||||
s.name = fmt.Sprintf("%s.%s", pkg.Name, s.name)
|
||||
}
|
||||
if x.Doc != nil {
|
||||
s.doc = x.Doc.Text()
|
||||
}
|
||||
p.structs[s.name] = s
|
||||
case *ast.InterfaceType:
|
||||
continue
|
||||
default: // for alias, such as `type MyInt int`
|
||||
alias := ts.Name.String()
|
||||
if pkg.Name != "main" {
|
||||
alias = fmt.Sprintf("%s.%s", pkg.Name, alias)
|
||||
}
|
||||
name, err := p.getModelName(t)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
p.alias[alias] = &astNode{
|
||||
name: name,
|
||||
expr: t,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// gets the true type (ast node) of the alias
|
||||
for alias := range p.alias {
|
||||
n := p.recursiveAlias(alias)
|
||||
if n != nil {
|
||||
p.alias[alias] = n
|
||||
}
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func walkGoSrc(dir string) (map[string]*ast.Package, error) {
|
||||
info, err := os.Stat(dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !info.IsDir() {
|
||||
return nil, errors.Errorf("%q is not a directory", dir)
|
||||
}
|
||||
|
||||
fset := token.NewFileSet()
|
||||
pkgs := make(map[string]*ast.Package)
|
||||
walk := func(path string, info fs.FileInfo, err error) error {
|
||||
if !info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
tmp, err := parser.ParseDir(fset, path, nil, parser.ParseComments)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for k, v := range tmp {
|
||||
pkgs[k] = v
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if err := filepath.Walk(dir, walk); err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to walk path %q", dir)
|
||||
}
|
||||
return pkgs, nil
|
||||
}
|
||||
|
||||
func (p *ModelParser) recursiveAlias(alias string) *astNode {
|
||||
if s, ok := p.structs[alias]; ok {
|
||||
return s
|
||||
}
|
||||
if n, ok := p.alias[alias]; ok {
|
||||
if n.name != alias {
|
||||
ret := p.recursiveAlias(n.name)
|
||||
if ret != nil {
|
||||
return ret
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetModel return the specified model
|
||||
func (p *ModelParser) GetModel(model string) (*Model, error) {
|
||||
fields, err := p.parseModelFields(model)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m := &Model{
|
||||
Name: model,
|
||||
Type: "object",
|
||||
Fields: fields,
|
||||
}
|
||||
m.setDoc(p.structs[model].doc)
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (p *ModelParser) parseModelFields(model string) (fields []Model, err error) {
|
||||
var s *astNode
|
||||
if _, ok := p.structs[model]; ok {
|
||||
s = p.structs[model]
|
||||
} else if _, ok = p.alias[model]; ok {
|
||||
s = p.alias[model]
|
||||
} else {
|
||||
return nil, ErrInvalidModel
|
||||
}
|
||||
|
||||
st, ok := s.expr.(*ast.StructType)
|
||||
if !ok || st.Fields == nil {
|
||||
return nil, ErrInvalidModel
|
||||
}
|
||||
pkgName := ""
|
||||
if idx := strings.Index(model, "."); idx != -1 {
|
||||
pkgName = model[:idx+1] // pkgName includes "."
|
||||
}
|
||||
for _, field := range st.Fields.List {
|
||||
if skipField(field) {
|
||||
continue
|
||||
}
|
||||
fd := Model{Name: field.Names[0].String()}
|
||||
if field.Doc != nil {
|
||||
fd.setDoc(field.Doc.Text())
|
||||
}
|
||||
if field.Tag != nil {
|
||||
ignore, err := fd.setTag(field.Tag.Value)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to parse tag %q of the field %q", fd.Tag, fd.Name)
|
||||
}
|
||||
if ignore {
|
||||
continue
|
||||
}
|
||||
}
|
||||
fd.Type, err = p.parseFiledType(pkgName, field.Type)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if IsObject(fd.Type) {
|
||||
subModel, err := p.getModelName(field.Type)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fd.Fields, err = p.parseModelFields(subModel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
fields = append(fields, fd)
|
||||
}
|
||||
return fields, nil
|
||||
}
|
||||
|
||||
func skipField(field *ast.Field) bool {
|
||||
name := field.Names
|
||||
return field == nil || name == nil || len(name) < 1 || name[0] == nil || name[0].String() == "_"
|
||||
}
|
||||
|
||||
func (m *Model) setDoc(str string) {
|
||||
m.Doc = strings.TrimSpace(str)
|
||||
}
|
||||
|
||||
func (m *Model) setTag(str string) (bool, error) {
|
||||
str = strings.Trim(str, "` ")
|
||||
if str == "" {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
ignore := false
|
||||
tag, err := structtag.Parse(str)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
m.Tag = str
|
||||
val, err := tag.Get("yaml")
|
||||
if err == nil {
|
||||
if val.Name == "-" || val.Name == "" {
|
||||
ignore = true
|
||||
}
|
||||
}
|
||||
return ignore, nil
|
||||
}
|
||||
|
||||
func (p *ModelParser) getModelName(typ ast.Expr) (string, error) {
|
||||
return p.doGetModelName("", typ)
|
||||
}
|
||||
|
||||
func (p *ModelParser) doGetModelName(pkgName string, typ ast.Expr) (string, error) {
|
||||
switch t := typ.(type) {
|
||||
case *ast.StarExpr: // *int -> int
|
||||
return p.doGetModelName(pkgName, t.X)
|
||||
case *ast.ArrayType: // slice or array
|
||||
return p.doGetModelName(pkgName, t.Elt)
|
||||
case *ast.SelectorExpr: // <pkg_name>.<field_name>
|
||||
pkg, ok := t.X.(*ast.Ident)
|
||||
if !ok {
|
||||
return "", ErrInvalidFiledType
|
||||
}
|
||||
pName := pkg.Name + "."
|
||||
return p.doGetModelName(pName, t.Sel)
|
||||
case *ast.Ident:
|
||||
return pkgName + t.Name, nil
|
||||
default:
|
||||
return "", ErrInvalidFiledType
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ModelParser) parseFiledType(pkgName string, typ ast.Expr) (string, error) {
|
||||
switch t := typ.(type) {
|
||||
case *ast.StructType: // nested struct
|
||||
return string(JsonTypeObject), nil
|
||||
case *ast.StarExpr: // *int -> int
|
||||
return p.parseFiledType(pkgName, t.X)
|
||||
case *ast.ArrayType: // slice or array
|
||||
ret, err := p.parseFiledType(pkgName, t.Elt)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return ArrayPrefix + ret, nil
|
||||
case *ast.SelectorExpr: // <pkg_name>.<field_name>
|
||||
pkg, ok := t.X.(*ast.Ident)
|
||||
if !ok {
|
||||
return "", ErrInvalidFiledType
|
||||
}
|
||||
pName := pkg.Name + "."
|
||||
return p.parseFiledType(pName, t.Sel)
|
||||
case *ast.Ident:
|
||||
fName := pkgName + t.Name
|
||||
if _, ok := p.structs[fName]; ok {
|
||||
return string(JsonTypeObject), nil
|
||||
}
|
||||
if alias, ok := p.alias[fName]; ok {
|
||||
return p.parseFiledType(pkgName, alias.expr)
|
||||
}
|
||||
jsonType, err := convert2JsonType(t.Name)
|
||||
return string(jsonType), err
|
||||
default:
|
||||
return "", ErrInvalidFiledType
|
||||
}
|
||||
}
|
||||
|
||||
func convert2JsonType(typ string) (JsonType, error) {
|
||||
switch typ {
|
||||
case "int", "int8", "int16", "int32", "int64",
|
||||
"uint", "uint8", "uint16", "uint32", "uint64":
|
||||
return JsonTypeInteger, nil
|
||||
case "float32", "float64":
|
||||
return JsonTypeNumber, nil
|
||||
case "bool":
|
||||
return JsonTypeBoolean, nil
|
||||
case "string":
|
||||
return JsonTypeString, nil
|
||||
case "struct":
|
||||
return JsonTypeObject, nil
|
||||
default:
|
||||
return "", ErrInvalidFiledType
|
||||
}
|
||||
}
|
||||
|
||||
type JsonType string
|
||||
|
||||
const (
|
||||
JsonTypeInteger JsonType = "integer"
|
||||
JsonTypeNumber JsonType = "number"
|
||||
JsonTypeBoolean JsonType = "boolean"
|
||||
JsonTypeString JsonType = "string"
|
||||
JsonTypeObject JsonType = "object"
|
||||
JsonTypeArray JsonType = "array"
|
||||
)
|
||||
379
pkg/cmd/hgctl/plugin/types/model_parser_test.go
Normal file
379
pkg/cmd/hgctl/plugin/types/model_parser_test.go
Normal file
@@ -0,0 +1,379 @@
|
||||
// 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 types
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGetModel(t *testing.T) {
|
||||
var (
|
||||
BasicStructField = []Model{
|
||||
{
|
||||
Name: "Name",
|
||||
Type: "string",
|
||||
},
|
||||
{
|
||||
Name: "Age",
|
||||
Type: "integer",
|
||||
},
|
||||
{
|
||||
Name: "Married",
|
||||
Type: "boolean",
|
||||
},
|
||||
{
|
||||
Name: "Salary",
|
||||
Type: "number",
|
||||
},
|
||||
}
|
||||
|
||||
ExternalStructField = []Model{
|
||||
{
|
||||
Name: "one",
|
||||
Type: "string",
|
||||
},
|
||||
{
|
||||
Name: "two",
|
||||
Type: "integer",
|
||||
},
|
||||
{
|
||||
Name: "three",
|
||||
Type: "array of boolean",
|
||||
},
|
||||
}
|
||||
|
||||
NestedStructField = []Model{
|
||||
{
|
||||
Name: "Simple",
|
||||
Type: "string",
|
||||
},
|
||||
{
|
||||
Name: "Complex",
|
||||
Type: "array of integer",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
expected *Model
|
||||
errMsg string
|
||||
}{
|
||||
{
|
||||
name: "TestBasicStruct",
|
||||
expected: &Model{
|
||||
Name: "TestBasicStruct",
|
||||
Type: "object",
|
||||
Fields: BasicStructField,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "TestComplexStruct",
|
||||
expected: &Model{
|
||||
Name: "TestComplexStruct",
|
||||
Type: "object",
|
||||
Fields: []Model{
|
||||
{
|
||||
Name: "Array",
|
||||
Type: "array of integer",
|
||||
},
|
||||
{
|
||||
Name: "Slice",
|
||||
Type: "array of string",
|
||||
},
|
||||
{
|
||||
Name: "Pointer",
|
||||
Type: "string",
|
||||
},
|
||||
{
|
||||
Name: "PPPointer",
|
||||
Type: "boolean",
|
||||
},
|
||||
{
|
||||
Name: "ArrayPointer",
|
||||
Type: "array of integer",
|
||||
},
|
||||
{
|
||||
Name: "SlicePointer",
|
||||
Type: "array of integer",
|
||||
},
|
||||
{
|
||||
Name: "StructPointerSlice",
|
||||
Type: "array of object",
|
||||
Fields: BasicStructField,
|
||||
},
|
||||
{
|
||||
Name: "StructArrayPointer",
|
||||
Type: "array of object",
|
||||
Fields: BasicStructField,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "TestAliasStruct",
|
||||
expected: &Model{
|
||||
Name: "TestAliasStruct",
|
||||
Type: "object",
|
||||
Fields: []Model{
|
||||
{
|
||||
Name: "MyString",
|
||||
Type: "string",
|
||||
},
|
||||
{
|
||||
Name: "MyPointerInt",
|
||||
Type: "integer",
|
||||
},
|
||||
{
|
||||
Name: "MyStruct",
|
||||
Type: "object",
|
||||
Fields: BasicStructField,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "TestExternalStruct",
|
||||
expected: &Model{
|
||||
Name: "TestExternalStruct",
|
||||
Type: "object",
|
||||
Fields: []Model{
|
||||
{
|
||||
Name: "InternalFloat",
|
||||
Type: "number",
|
||||
},
|
||||
{
|
||||
Name: "ExStruct",
|
||||
Type: "object",
|
||||
Fields: ExternalStructField,
|
||||
},
|
||||
{
|
||||
Name: "ExternalInt",
|
||||
Type: "integer",
|
||||
},
|
||||
{
|
||||
Name: "ExBool",
|
||||
Type: "boolean",
|
||||
},
|
||||
{
|
||||
Name: "ExSlice",
|
||||
Type: "array of string",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "TestNestedStruct",
|
||||
expected: &Model{
|
||||
Name: "TestNestedStruct",
|
||||
Type: "object",
|
||||
Fields: []Model{
|
||||
{
|
||||
Name: "NestedStruct",
|
||||
Type: "object",
|
||||
Fields: []Model{
|
||||
{
|
||||
Name: "NestedStruct",
|
||||
Type: "object",
|
||||
Fields: NestedStructField,
|
||||
},
|
||||
{
|
||||
Name: "NestedInt",
|
||||
Type: "integer",
|
||||
},
|
||||
{
|
||||
Name: "NestedString",
|
||||
Type: "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ext.TestExStruct",
|
||||
expected: &Model{
|
||||
Name: "ext.TestExStruct",
|
||||
Type: "object",
|
||||
Fields: ExternalStructField,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ext.TestNestedStruct",
|
||||
expected: &Model{
|
||||
Name: "ext.TestNestedStruct",
|
||||
Type: "object",
|
||||
Fields: []Model{
|
||||
{
|
||||
Name: "NestedStruct",
|
||||
Type: "object",
|
||||
Fields: NestedStructField,
|
||||
},
|
||||
{
|
||||
Name: "NestedInt",
|
||||
Type: "integer",
|
||||
},
|
||||
{
|
||||
Name: "NestedString",
|
||||
Type: "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
p, err := NewModelParser("./testdata/types")
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
actual, err := p.GetModel(c.name)
|
||||
if c.errMsg != "" {
|
||||
require.EqualError(t, err, c.errMsg)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, c.expected, actual)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseStructAndAlias(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
dir string
|
||||
expectedStructs map[string]struct{}
|
||||
expectedAlias map[string]string
|
||||
}{
|
||||
{
|
||||
name: "Basic",
|
||||
dir: "./testdata/types",
|
||||
expectedStructs: map[string]struct{}{
|
||||
"TestBasicStruct": {},
|
||||
"TestComplexStruct": {},
|
||||
"TestAliasStruct": {},
|
||||
"TestExternalStruct": {},
|
||||
"TestNestedStruct": {},
|
||||
"ext.TestExStruct": {},
|
||||
"ext.TestNestedStruct": {},
|
||||
"nested.TestNestedStruct": {},
|
||||
},
|
||||
expectedAlias: map[string]string{
|
||||
"MyString": "string",
|
||||
"MyPointerInt": "int",
|
||||
"MyStruct": "TestBasicStruct",
|
||||
"NestedAlias": "nested.TestNestedStruct",
|
||||
"NestedBasicAlias": "bool",
|
||||
"ext.ExAlias": "nested.TestNestedStruct",
|
||||
"ext.ExPointerInt": "int",
|
||||
"ext.ExBool": "bool",
|
||||
"ext.ExSlice": "string",
|
||||
"nested.NestedInt": "int",
|
||||
"nested.NestedString": "string",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
p, err := NewModelParser(c.dir)
|
||||
require.NoError(t, err)
|
||||
|
||||
actualStructs := make(map[string]struct{})
|
||||
for _, s := range p.structs {
|
||||
actualStructs[s.name] = struct{}{}
|
||||
}
|
||||
require.Equal(t, c.expectedStructs, actualStructs)
|
||||
|
||||
actualAlias := make(map[string]string)
|
||||
for name, alias := range p.alias {
|
||||
actualAlias[name] = alias.name
|
||||
}
|
||||
require.Equal(t, c.expectedAlias, actualAlias)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStructFieldDocAndTag(t *testing.T) {
|
||||
var BasicStructField = []Model{
|
||||
{
|
||||
Name: "Name",
|
||||
Type: "string",
|
||||
Doc: "Name, specify username",
|
||||
Tag: `yaml:"name" required:"true" minLength:"1" maxLength:"32"`,
|
||||
},
|
||||
{
|
||||
Name: "Age",
|
||||
Type: "integer",
|
||||
Doc: "Age, specify age",
|
||||
Tag: `yaml:"age" required:"true" minimum:"0" maximum:"140"`,
|
||||
},
|
||||
{
|
||||
Name: "Married",
|
||||
Type: "boolean",
|
||||
Doc: "Married, specify marital status [true, false]\nand optional",
|
||||
Tag: `yaml:"married" required:"false"`,
|
||||
},
|
||||
{
|
||||
Name: "Salary",
|
||||
Type: "number",
|
||||
Doc: "Salary, specify income status, optional",
|
||||
Tag: `yaml:"salary" required:"false"`,
|
||||
},
|
||||
{
|
||||
Name: "Children",
|
||||
Type: "array of string",
|
||||
Doc: "Children, specify a list of children's names, optional",
|
||||
Tag: `yaml:"children" required:"false"`,
|
||||
},
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
model string
|
||||
expected []Model
|
||||
}{
|
||||
{
|
||||
name: "TestBasicDocTag",
|
||||
model: "TestBasicDocTag",
|
||||
expected: BasicStructField,
|
||||
},
|
||||
{
|
||||
name: "TestNestedStructDocTag",
|
||||
model: "TestNestedStructDocTag",
|
||||
expected: []Model{
|
||||
{
|
||||
Name: "Struct",
|
||||
Type: "array of object",
|
||||
Doc: "This is the comment of the nested struct field",
|
||||
Tag: `yaml:"struct" required:"true" minItems:"1" maxItems:"10"`,
|
||||
Fields: BasicStructField,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
p, err := NewModelParser("./testdata/doc_tag")
|
||||
require.NoError(t, err)
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
m, err := p.GetModel(c.model)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, c.expected, m.Fields)
|
||||
})
|
||||
}
|
||||
}
|
||||
426
pkg/cmd/hgctl/plugin/types/schema.go
Normal file
426
pkg/cmd/hgctl/plugin/types/schema.go
Normal file
@@ -0,0 +1,426 @@
|
||||
// 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 types
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/plugin/utils"
|
||||
|
||||
"github.com/fatih/structtag"
|
||||
"github.com/iancoleman/orderedmap"
|
||||
)
|
||||
|
||||
// JSONSchemaProps is a JSON-Schema following Specification Draft 4 (http://json-schema.org/).
|
||||
// Borrowed from https://github.com/kubernetes/apiextensions-apiserver/blob/master/pkg/apis/apiextensions/v1/types_jsonschema.go
|
||||
type JSONSchemaProps struct {
|
||||
ID string `json:"id,omitempty" yaml:"id,omitempty"`
|
||||
Schema JSONSchemaURL `json:"$schema,omitempty" yaml:"$schema,omitempty"`
|
||||
Ref *string `json:"$ref,omitempty" yaml:"$ref,omitempty"`
|
||||
Type string `json:"type,omitempty" yaml:"type,omitempty"`
|
||||
Format string `json:"format,omitempty" yaml:"format,omitempty"`
|
||||
Scope Scope `json:"scope,omitempty" yaml:"scope,omitempty"`
|
||||
Title string `json:"title,omitempty" yaml:"title,omitempty"`
|
||||
XTitleI18n map[I18nType]string `json:"x-title-i18n,omitempty" yaml:"x-title-i18n,omitempty"`
|
||||
Description string `json:"description,omitempty" yaml:"description,omitempty"`
|
||||
XDescriptionI18n map[I18nType]string `json:"x-description-i18n,omitempty" yaml:"x-description-i18n,omitempty"`
|
||||
Default *JSON `json:"default,omitempty" yaml:"default,omitempty"`
|
||||
Minimum *float64 `json:"minimum,omitempty" yaml:"minimum,omitempty"`
|
||||
ExclusiveMinimum bool `json:"exclusiveMinimum,omitempty" yaml:"exclusiveMinimum,omitempty"`
|
||||
Maximum *float64 `json:"maximum,omitempty" yaml:"maximum,omitempty"`
|
||||
ExclusiveMaximum bool `json:"exclusiveMaximum,omitempty" yaml:"exclusiveMaximum,omitempty"`
|
||||
MinLength *int64 `json:"minLength,omitempty" yaml:"minLength,omitempty"`
|
||||
MaxLength *int64 `json:"maxLength,omitempty" yaml:"maxLength,omitempty"`
|
||||
Pattern string `json:"pattern,omitempty" yaml:"pattern,omitempty"`
|
||||
MaxItems *int64 `json:"maxItems,omitempty" yaml:"maxItems,omitempty"`
|
||||
MinItems *int64 `json:"minItems,omitempty" yaml:"minItems,omitempty"`
|
||||
UniqueItems bool `json:"uniqueItems,omitempty" yaml:"uniqueItems,omitempty"`
|
||||
MultipleOf *float64 `json:"multipleOf,omitempty" yaml:"multipleOf,omitempty"`
|
||||
Enum []JSON `json:"enum,omitempty" yaml:"enum,omitempty"`
|
||||
MinProperties *int64 `json:"minProperties,omitempty" yaml:"minProperties,omitempty"`
|
||||
MaxProperties *int64 `json:"maxProperties,omitempty" yaml:"maxProperties,omitempty"`
|
||||
Required []string `json:"required,omitempty" yaml:"required,omitempty"`
|
||||
Items *JSONSchemaPropsOrArray `json:"items,omitempty" yaml:"items,omitempty"`
|
||||
AllOf []JSONSchemaProps `json:"allOf,omitempty" yaml:"allOf,omitempty"`
|
||||
OneOf []JSONSchemaProps `json:"oneOf,omitempty" yaml:"oneOf,omitempty"`
|
||||
AnyOf []JSONSchemaProps `json:"anyOf,omitempty" yaml:"anyOf,omitempty"`
|
||||
Not *JSONSchemaProps `json:"not,omitempty" yaml:"not,omitempty"`
|
||||
Properties map[string]JSONSchemaProps `json:"properties,omitempty" yaml:"properties,omitempty"`
|
||||
AdditionalProperties *JSONSchemaPropsOrBool `json:"additionalProperties,omitempty" yaml:"additionalProperties,omitempty"`
|
||||
PatternProperties map[string]JSONSchemaProps `json:"patternProperties,omitempty" yaml:"patternProperties,omitempty"`
|
||||
Dependencies JSONSchemaDependencies `json:"dependencies,omitempty" yaml:"dependencies,omitempty"`
|
||||
AdditionalItems *JSONSchemaPropsOrBool `json:"additionalItems,omitempty" yaml:"additionalItems,omitempty"`
|
||||
Definitions JSONSchemaDefinitions `json:"definitions,omitempty" yaml:"definitions,omitempty"`
|
||||
ExternalDocs *ExternalDocumentation `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"`
|
||||
Example *JSON `json:"example,omitempty" yaml:"example,omitempty"`
|
||||
Nullable bool `json:"nullable,omitempty" yaml:"nullable,omitempty"`
|
||||
}
|
||||
|
||||
type Scope string
|
||||
|
||||
const (
|
||||
ScopeGlobal Scope = "GLOBAL"
|
||||
ScopeInstance Scope = "INSTANCE"
|
||||
ScopeAll Scope = "ALL"
|
||||
ScopeDefault = ScopeInstance
|
||||
)
|
||||
|
||||
// JSON represents any valid JSON value.
|
||||
// These types are supported: bool, int64, float64, string, []interface{}, map[string]interface{} and nil.
|
||||
type JSON struct {
|
||||
Raw []byte `json:"-" yaml:"-"`
|
||||
}
|
||||
|
||||
// JSONSchemaPropsOrArray represents a value that can either be a JSONSchemaProps
|
||||
// or an array of JSONSchemaProps. Mainly here for serialization purposes.
|
||||
type JSONSchemaPropsOrArray struct {
|
||||
Schema *JSONSchemaProps
|
||||
JSONSchemas []JSONSchemaProps
|
||||
}
|
||||
|
||||
// JSONSchemaPropsOrBool represents JSONSchemaProps or a boolean value.
|
||||
// Defaults to true for the boolean property.
|
||||
type JSONSchemaPropsOrBool struct {
|
||||
Allows bool
|
||||
Schema *JSONSchemaProps
|
||||
}
|
||||
|
||||
// JSONSchemaDependencies represent a dependencies property.
|
||||
type JSONSchemaDependencies map[string]JSONSchemaPropsOrStringArray
|
||||
|
||||
// JSONSchemaPropsOrStringArray represents a JSONSchemaProps or a string array.
|
||||
type JSONSchemaPropsOrStringArray struct {
|
||||
Schema *JSONSchemaProps
|
||||
Property []string
|
||||
}
|
||||
|
||||
// JSONSchemaURL represents a schema url.
|
||||
type JSONSchemaURL string
|
||||
|
||||
// JSONSchemaDefinitions contains the models explicitly defined in this spec.
|
||||
type JSONSchemaDefinitions map[string]JSONSchemaProps
|
||||
|
||||
// ExternalDocumentation allows referencing an external resource for extended documentation.
|
||||
type ExternalDocumentation struct {
|
||||
Description string `json:"description,omitempty" yaml:"description,omitempty"`
|
||||
URL string `json:"url,omitempty" yaml:"url,omitempty"`
|
||||
}
|
||||
|
||||
func NewJSONSchemaProps() *JSONSchemaProps {
|
||||
return &JSONSchemaProps{
|
||||
XTitleI18n: make(map[I18nType]string),
|
||||
XDescriptionI18n: make(map[I18nType]string),
|
||||
Properties: make(map[string]JSONSchemaProps),
|
||||
}
|
||||
}
|
||||
|
||||
// IsRequired determines whether the given `name` field is required
|
||||
func (s *JSONSchemaProps) IsRequired(name string) bool {
|
||||
req := false
|
||||
for _, n := range s.Required {
|
||||
if name == n {
|
||||
req = true
|
||||
break
|
||||
}
|
||||
}
|
||||
return req
|
||||
}
|
||||
|
||||
// GetDefaultValue returns the default value of the schema
|
||||
func (s *JSONSchemaProps) GetDefaultValue() string {
|
||||
d := "-"
|
||||
if s.Default == nil {
|
||||
return d
|
||||
}
|
||||
if len(s.Default.Raw) > 0 {
|
||||
d = string(s.Default.Raw)
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
// GetExample returns the pretty example of the schema
|
||||
func (s *JSONSchemaProps) GetExample() string {
|
||||
ret := ""
|
||||
if s.Example != nil && len(s.Example.Raw) > 0 {
|
||||
ret = string(s.Example.Raw)
|
||||
if ret[0] == '{' {
|
||||
// string(s.Example.Raw) might look like (when the schema is generated through go src):
|
||||
// {"allow":["consumer1"],"consumers":[{"credential":"admin:123456","name":"consumer1"}]}
|
||||
var obj interface{}
|
||||
err := json.Unmarshal(s.Example.Raw, &obj)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
b, err := utils.MarshalYamlWithIndent(obj, 2)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
ret = string(b)
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// GetPropertiesOrderMap converts the schema Properties map to
|
||||
// an ordered map (dictionary order) and returns it
|
||||
func (s *JSONSchemaProps) GetPropertiesOrderMap() *orderedmap.OrderedMap {
|
||||
m := orderedmap.New()
|
||||
for name, prop := range s.Properties {
|
||||
m.Set(name, prop)
|
||||
}
|
||||
m.SortKeys(sort.Strings)
|
||||
return m
|
||||
}
|
||||
|
||||
// HandleFieldAnnotations parses the comment (annotations look like `// @<KEY> [LANGUAGE] <VALUE>`)
|
||||
// and sets the schema properties
|
||||
func (s *JSONSchemaProps) HandleFieldAnnotations(comment string) {
|
||||
as := GetAnnotations(comment)
|
||||
for _, a := range as {
|
||||
switch a.Type {
|
||||
case ATitle:
|
||||
if s.Title == "" {
|
||||
s.Title = a.Text
|
||||
}
|
||||
s.XTitleI18n[a.I18nType] = a.Text
|
||||
case ADescription:
|
||||
if s.Description == "" {
|
||||
s.Description = a.Text
|
||||
}
|
||||
s.XDescriptionI18n[a.I18nType] = a.Text
|
||||
case AScope:
|
||||
s.Scope = Scope(a.Text)
|
||||
case AExample:
|
||||
s.Example = &JSON{Raw: []byte(a.Text)}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// HandleFieldTags parses the struct field tags and sets the schema properties
|
||||
// TODO: Add more tags (now supported yaml, minimum, maximum, ...)
|
||||
func (s *JSONSchemaProps) HandleFieldTags(tags string, parent *JSONSchemaProps, fieldName string) string {
|
||||
if tags == "" {
|
||||
return fieldName
|
||||
}
|
||||
st, err := structtag.Parse(tags)
|
||||
if err != nil {
|
||||
return fieldName
|
||||
}
|
||||
|
||||
newName := fieldName
|
||||
for _, tag := range st.Tags() {
|
||||
switch tag.Key {
|
||||
case "yaml":
|
||||
newName = tag.Name
|
||||
if s.Title == "" {
|
||||
s.Title = newName
|
||||
s.XTitleI18n[I18nDefault] = newName
|
||||
}
|
||||
case "required":
|
||||
required, _ := strconv.ParseBool(tag.Name)
|
||||
if !required {
|
||||
continue
|
||||
}
|
||||
parent.Required = append(parent.Required, newName)
|
||||
case "minimum":
|
||||
min, err := strconv.ParseFloat(tag.Name, 64)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
s.Minimum = &min
|
||||
case "maximum":
|
||||
max, err := strconv.ParseFloat(tag.Name, 64)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
s.Maximum = &max
|
||||
case "minLength":
|
||||
minL, err := strconv.ParseInt(tag.Name, 10, 64)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
s.MinLength = &minL
|
||||
case "maxLength":
|
||||
maxL, err := strconv.ParseInt(tag.Name, 10, 64)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
s.MaxLength = &maxL
|
||||
case "minItems":
|
||||
minI, err := strconv.ParseInt(tag.Name, 10, 64)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
s.MinItems = &minI
|
||||
case "maxItems":
|
||||
maxI, err := strconv.ParseInt(tag.Name, 10, 64)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
s.MaxItems = &maxI
|
||||
case "pattern":
|
||||
s.Pattern = tag.Name
|
||||
}
|
||||
}
|
||||
|
||||
return newName
|
||||
}
|
||||
|
||||
// JoinRequirementsBy joins the requirements by the given i18n type. Return value looks like:
|
||||
// required, minLength 10, regular expression "^.*$"
|
||||
func (s *JSONSchemaProps) JoinRequirementsBy(i18n I18nType, required bool) string {
|
||||
reqs := s.getRequirements(required)
|
||||
switch i18n {
|
||||
case I18nZH_CN:
|
||||
return strings.Join(reqs[I18nZH_CN], ",")
|
||||
case I18nEN_US:
|
||||
fallthrough
|
||||
default:
|
||||
return strings.Join(reqs[I18nDefault], ", ")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *JSONSchemaProps) getRequirements(required bool) map[I18nType][]string {
|
||||
reqs := make(map[I18nType][]string)
|
||||
|
||||
for i18n, str := range s.GetRequired(required) {
|
||||
reqs[i18n] = append(reqs[i18n], str)
|
||||
}
|
||||
|
||||
for i18n, str := range s.GetMinimum() {
|
||||
reqs[i18n] = append(reqs[i18n], str)
|
||||
}
|
||||
|
||||
for i18n, str := range s.GetMaximum() {
|
||||
reqs[i18n] = append(reqs[i18n], str)
|
||||
}
|
||||
|
||||
for i18n, str := range s.GetMinLength() {
|
||||
reqs[i18n] = append(reqs[i18n], str)
|
||||
}
|
||||
|
||||
for i18n, str := range s.GetMaxLength() {
|
||||
reqs[i18n] = append(reqs[i18n], str)
|
||||
}
|
||||
|
||||
for i18n, str := range s.GetMinItems() {
|
||||
reqs[i18n] = append(reqs[i18n], str)
|
||||
}
|
||||
|
||||
for i18n, str := range s.GetMaxItems() {
|
||||
reqs[i18n] = append(reqs[i18n], str)
|
||||
}
|
||||
|
||||
for i18n, str := range s.GetPattern() {
|
||||
reqs[i18n] = append(reqs[i18n], str)
|
||||
}
|
||||
|
||||
return reqs
|
||||
}
|
||||
|
||||
func (s *JSONSchemaProps) GetMinimum() map[I18nType]string {
|
||||
if s.Minimum == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return map[I18nType]string{
|
||||
I18nZH_CN: fmt.Sprintf("最小值 %f", *s.Minimum),
|
||||
I18nEN_US: fmt.Sprintf("minimum %f", *s.Minimum),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *JSONSchemaProps) GetMaximum() map[I18nType]string {
|
||||
if s.Maximum == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return map[I18nType]string{
|
||||
I18nZH_CN: fmt.Sprintf("最大值 %f", *s.Maximum),
|
||||
I18nEN_US: fmt.Sprintf("maximum %f", *s.Maximum),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *JSONSchemaProps) GetMinLength() map[I18nType]string {
|
||||
if s.MinLength == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return map[I18nType]string{
|
||||
I18nZH_CN: fmt.Sprintf("最小长度 %d", *s.MinLength),
|
||||
I18nEN_US: fmt.Sprintf("minLength %d", *s.MinLength),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *JSONSchemaProps) GetMaxLength() map[I18nType]string {
|
||||
if s.MaxLength == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return map[I18nType]string{
|
||||
I18nZH_CN: fmt.Sprintf("最大长度 %d", *s.MaxLength),
|
||||
I18nEN_US: fmt.Sprintf("maxLength %d", *s.MaxLength),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *JSONSchemaProps) GetPattern() map[I18nType]string {
|
||||
if s.Pattern == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
return map[I18nType]string{
|
||||
I18nZH_CN: fmt.Sprintf("正则表达式 %q", s.Pattern),
|
||||
I18nEN_US: fmt.Sprintf("regular expression %q", s.Pattern),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *JSONSchemaProps) GetMinItems() map[I18nType]string {
|
||||
if s.MinItems == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return map[I18nType]string{
|
||||
I18nZH_CN: fmt.Sprintf("最小 item 个数 %d", *s.MinItems),
|
||||
I18nEN_US: fmt.Sprintf("minItems %d", *s.MinItems),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *JSONSchemaProps) GetMaxItems() map[I18nType]string {
|
||||
if s.MaxItems == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return map[I18nType]string{
|
||||
I18nZH_CN: fmt.Sprintf("最大 item 个数 %d", *s.MaxItems),
|
||||
I18nEN_US: fmt.Sprintf("maxItems %d", *s.MaxItems),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *JSONSchemaProps) GetRequired(req bool) map[I18nType]string {
|
||||
if req {
|
||||
return map[I18nType]string{
|
||||
I18nZH_CN: "必填",
|
||||
I18nEN_US: "required",
|
||||
}
|
||||
}
|
||||
|
||||
return map[I18nType]string{
|
||||
I18nZH_CN: "选填",
|
||||
I18nEN_US: "optional",
|
||||
}
|
||||
}
|
||||
45
pkg/cmd/hgctl/plugin/types/testdata/doc_tag/main.go
vendored
Normal file
45
pkg/cmd/hgctl/plugin/types/testdata/doc_tag/main.go
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
// 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 main
|
||||
|
||||
// TestBasicDocTag This is a test struct for documents(comments) and tags
|
||||
type TestBasicDocTag struct {
|
||||
// Name, specify username
|
||||
Name string `yaml:"name" required:"true" minLength:"1" maxLength:"32"`
|
||||
|
||||
// Age, specify age
|
||||
Age uint `yaml:"age" required:"true" minimum:"0" maximum:"140" `
|
||||
|
||||
// Married, specify marital status [true, false]
|
||||
// and optional
|
||||
Married bool `yaml:"married" required:"false"`
|
||||
|
||||
// Salary, specify income status, optional
|
||||
Salary float64 `yaml:"salary" required:"false"`
|
||||
|
||||
// Children, specify a list of children's names, optional
|
||||
Children []string `yaml:"children" required:"false"`
|
||||
|
||||
// ignore1
|
||||
Ignore1 string `yaml:"-"`
|
||||
|
||||
// ignore 2
|
||||
Ignore2 string `yaml:""`
|
||||
}
|
||||
|
||||
type TestNestedStructDocTag struct {
|
||||
// This is the comment of the nested struct field
|
||||
Struct []*TestBasicDocTag `yaml:"struct" required:"true" minItems:"1" maxItems:"10"`
|
||||
}
|
||||
34
pkg/cmd/hgctl/plugin/types/testdata/types/ext/ext.go
vendored
Normal file
34
pkg/cmd/hgctl/plugin/types/testdata/types/ext/ext.go
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
// 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 ext
|
||||
|
||||
import "github.com/alibaba/higress/pkg/cmd/hgctl/plugin/types/testdata/types/ext/nested"
|
||||
|
||||
type TestExStruct struct {
|
||||
one string
|
||||
two *int
|
||||
three []bool
|
||||
}
|
||||
|
||||
type ExPointerInt **int
|
||||
type ExBool bool
|
||||
type ExSlice []*string
|
||||
type ExAlias nested.TestNestedStruct
|
||||
|
||||
type TestNestedStruct struct {
|
||||
NestedStruct *nested.TestNestedStruct
|
||||
NestedInt *nested.NestedInt
|
||||
NestedString nested.NestedString
|
||||
}
|
||||
23
pkg/cmd/hgctl/plugin/types/testdata/types/ext/nested/nested.go
vendored
Normal file
23
pkg/cmd/hgctl/plugin/types/testdata/types/ext/nested/nested.go
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
// 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 nested
|
||||
|
||||
type TestNestedStruct struct {
|
||||
Simple string
|
||||
Complex **[]*int
|
||||
}
|
||||
|
||||
type NestedInt ***int
|
||||
type NestedString string
|
||||
70
pkg/cmd/hgctl/plugin/types/testdata/types/main.go
vendored
Normal file
70
pkg/cmd/hgctl/plugin/types/testdata/types/main.go
vendored
Normal file
@@ -0,0 +1,70 @@
|
||||
// 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 main
|
||||
|
||||
import "github.com/alibaba/higress/pkg/cmd/hgctl/plugin/types/testdata/types/ext"
|
||||
|
||||
type TestBasicStruct struct {
|
||||
Name string
|
||||
Age uint
|
||||
Married bool
|
||||
Salary float64
|
||||
}
|
||||
|
||||
type TestComplexStruct struct {
|
||||
Array [2]int
|
||||
Slice []string
|
||||
Pointer *string
|
||||
PPPointer ***bool
|
||||
ArrayPointer [2]*int
|
||||
SlicePointer []*int
|
||||
StructPointerSlice []*TestBasicStruct
|
||||
StructArrayPointer *[]TestBasicStruct
|
||||
_ struct {
|
||||
one int
|
||||
two string
|
||||
}
|
||||
}
|
||||
|
||||
type TestAliasStruct struct {
|
||||
MyString *MyString
|
||||
MyPointerInt MyPointerInt
|
||||
MyStruct MyStruct
|
||||
}
|
||||
|
||||
type MyString string
|
||||
type MyPointerInt *int
|
||||
type MyStruct TestBasicStruct
|
||||
type NestedAlias ext.ExAlias
|
||||
type NestedBasicAlias ext.ExBool
|
||||
|
||||
type TestExternalStruct struct {
|
||||
InternalFloat float64
|
||||
ExStruct ext.TestExStruct
|
||||
ExternalInt ext.ExPointerInt
|
||||
ExBool ext.ExBool
|
||||
ExSlice ext.ExSlice
|
||||
}
|
||||
|
||||
type TestNestedStruct struct {
|
||||
NestedStruct *ext.TestNestedStruct
|
||||
}
|
||||
|
||||
type MyInterface interface {
|
||||
}
|
||||
|
||||
var MyConst bool
|
||||
|
||||
var MyVar int
|
||||
102
pkg/cmd/hgctl/plugin/uninstall/uninstall.go
Normal file
102
pkg/cmd/hgctl/plugin/uninstall/uninstall.go
Normal file
@@ -0,0 +1,102 @@
|
||||
// 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 uninstall
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
k8s "github.com/alibaba/higress/pkg/cmd/hgctl/kubernetes"
|
||||
"github.com/alibaba/higress/pkg/cmd/options"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
k8serr "k8s.io/apimachinery/pkg/api/errors"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
)
|
||||
|
||||
func NewCommand() *cobra.Command {
|
||||
var (
|
||||
name string
|
||||
all bool
|
||||
)
|
||||
|
||||
uninstallCmd := &cobra.Command{
|
||||
Use: "uninstall",
|
||||
Aliases: []string{"u", "uins"},
|
||||
Short: "Uninstall WASM plugin",
|
||||
Example: ` # Uninstall WASM plugin using the WasmPlugin name
|
||||
hgctl plugin uninstall -p example-plugin-name
|
||||
|
||||
# Uninstall all WASM plugins
|
||||
hgctl plugin uninstall -A
|
||||
`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.CheckErr(uninstall(cmd.OutOrStdout(), name, all))
|
||||
},
|
||||
}
|
||||
|
||||
flags := uninstallCmd.PersistentFlags()
|
||||
options.AddKubeConfigFlags(flags)
|
||||
k8s.AddHigressNamespaceFlags(flags)
|
||||
flags.StringVarP(&name, "name", "p", "", "Name of the WASM plugin you want to uninstall")
|
||||
flags.BoolVarP(&all, "all", "A", false, "Delete all installed WASM plugin")
|
||||
|
||||
return uninstallCmd
|
||||
}
|
||||
|
||||
func uninstall(w io.Writer, name string, all bool) error {
|
||||
dynCli, err := k8s.NewDynamicClient(options.DefaultConfigFlags.ToRawKubeConfigLoader())
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to build kubernetes dynamic client")
|
||||
}
|
||||
cli := k8s.NewWasmPluginClient(dynCli)
|
||||
|
||||
ctx := context.TODO()
|
||||
plugins := make([]string, 0)
|
||||
if all {
|
||||
list, err := cli.List(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to get information of all wasm plugins")
|
||||
}
|
||||
for _, item := range list.Items {
|
||||
plugins = append(plugins, item.GetName())
|
||||
}
|
||||
} else {
|
||||
plugins = append(plugins, name)
|
||||
}
|
||||
|
||||
for _, p := range plugins {
|
||||
err = deleteOne(ctx, w, cli, p)
|
||||
if err != nil {
|
||||
fmt.Fprintln(w, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func deleteOne(ctx context.Context, w io.Writer, cli *k8s.WasmPluginClient, name string) error {
|
||||
result, err := cli.Delete(ctx, name)
|
||||
if err != nil && k8serr.IsNotFound(err) {
|
||||
return errors.Errorf("wasm plugin %q is not found", fmt.Sprintf("%s/%s", k8s.HigressNamespace, name))
|
||||
} else if err != nil {
|
||||
return errors.Wrapf(err, "failed to uninstall wasm plugin %q", fmt.Sprintf("%s/%s", k8s.HigressNamespace, name))
|
||||
}
|
||||
|
||||
fmt.Fprintf(w, "Uninstalled wasm plugin %q\n", fmt.Sprintf("%s/%s", result.GetNamespace(), result.GetName()))
|
||||
return nil
|
||||
}
|
||||
92
pkg/cmd/hgctl/plugin/utils/common.go
Normal file
92
pkg/cmd/hgctl/plugin/utils/common.go
Normal file
@@ -0,0 +1,92 @@
|
||||
// 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 utils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/mitchellh/go-homedir"
|
||||
"github.com/pkg/errors"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// GetAbsolutePath returns the absolute path, e.g.:
|
||||
// - ~/foo -> /home/user/foo
|
||||
// - ./foo -> /current/dir/foo
|
||||
// - /foo/ -> /foo
|
||||
func GetAbsolutePath(path string) (newPath string, err error) {
|
||||
if strings.HasPrefix(path, "~") {
|
||||
newPath, err = homedir.Expand(path)
|
||||
if err != nil {
|
||||
return "", errors.Wrapf(err, "failed to expand path: %q", path)
|
||||
}
|
||||
} else {
|
||||
newPath, err = filepath.Abs(path)
|
||||
if err != nil {
|
||||
return "", errors.Wrapf(err, "failed to get absolute path of %q", path)
|
||||
}
|
||||
}
|
||||
|
||||
l := len(newPath)
|
||||
if l > 1 && newPath[l-1] == '/' { // if l == 1, the path might be "/"
|
||||
newPath = newPath[:l-1]
|
||||
}
|
||||
|
||||
return newPath, nil
|
||||
}
|
||||
|
||||
// AddIndent for each line of str
|
||||
func AddIndent(str, indent string) string {
|
||||
ret := ""
|
||||
ss := strings.Split(str, "\n")
|
||||
for i, s := range ss {
|
||||
if i == 0 {
|
||||
ret = fmt.Sprintf("%s%s", indent, s)
|
||||
} else {
|
||||
ret = fmt.Sprintf("%s\n%s%s", ret, indent, s)
|
||||
}
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
// MarshalYamlWithIndent marshals v to yaml with indent, specify space width with spaces
|
||||
func MarshalYamlWithIndent(v interface{}, spaces int) ([]byte, error) {
|
||||
w := new(bytes.Buffer)
|
||||
ec := yaml.NewEncoder(w)
|
||||
defer ec.Close()
|
||||
ec.SetIndent(spaces)
|
||||
if err := ec.Encode(v); err != nil {
|
||||
return w.Bytes(), err
|
||||
}
|
||||
|
||||
return w.Bytes(), nil
|
||||
}
|
||||
|
||||
// MarshalYamlWithIndentTo marshals v to yaml with indent, specify space width with spaces, and output to w
|
||||
func MarshalYamlWithIndentTo(w io.Writer, v interface{}, spaces int) error {
|
||||
ec := yaml.NewEncoder(w)
|
||||
defer ec.Close()
|
||||
ec.SetIndent(spaces)
|
||||
if err := ec.Encode(v); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
58
pkg/cmd/hgctl/plugin/utils/debugger.go
Normal file
58
pkg/cmd/hgctl/plugin/utils/debugger.go
Normal file
@@ -0,0 +1,58 @@
|
||||
// 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 utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
type Debugger interface {
|
||||
Debugf(format string, a ...any) (int, error)
|
||||
Debugln(a ...any) (int, error)
|
||||
}
|
||||
|
||||
type DefaultDebugger struct {
|
||||
debug bool
|
||||
w io.Writer
|
||||
}
|
||||
|
||||
func NewDefaultDebugger(debug bool, w io.Writer) *DefaultDebugger {
|
||||
return &DefaultDebugger{debug: debug, w: w}
|
||||
}
|
||||
|
||||
func (d DefaultDebugger) Debugf(format string, a ...any) (int, error) {
|
||||
l := len(format)
|
||||
if l > 0 && format[l-1] != '\n' {
|
||||
format += "\n"
|
||||
}
|
||||
if d.debug {
|
||||
format = "[debug] " + format
|
||||
return fmt.Fprintf(d.w, format, a...)
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func (d DefaultDebugger) Debugln(a ...any) (int, error) {
|
||||
if d.debug {
|
||||
n1, err1 := fmt.Fprintf(d.w, "[debug] ")
|
||||
if err1 != nil {
|
||||
return n1, err1
|
||||
}
|
||||
n2, err2 := fmt.Fprintln(d.w, a...)
|
||||
return n1 + n2, err2
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
165
pkg/cmd/hgctl/plugin/utils/printer.go
Normal file
165
pkg/cmd/hgctl/plugin/utils/printer.go
Normal file
@@ -0,0 +1,165 @@
|
||||
// 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 utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
)
|
||||
|
||||
type YesOrNoPrinter struct {
|
||||
out io.Writer
|
||||
indent *Indent
|
||||
yes, no *color.Color
|
||||
}
|
||||
|
||||
var (
|
||||
DefaultOut = os.Stdout
|
||||
DefaultIdent = NewIndent(strings.Repeat(" ", 2), 0)
|
||||
DefaultYes = color.New(color.FgHiGreen)
|
||||
DefaultNo = color.New(color.FgHiRed)
|
||||
)
|
||||
|
||||
func NewPrinter(out io.Writer, indent *Indent, yes, no *color.Color) *YesOrNoPrinter {
|
||||
return &YesOrNoPrinter{
|
||||
out: out,
|
||||
indent: indent,
|
||||
yes: yes,
|
||||
no: no,
|
||||
}
|
||||
}
|
||||
|
||||
func DefaultPrinter() *YesOrNoPrinter {
|
||||
return NewPrinter(DefaultOut, DefaultIdent, DefaultYes, DefaultNo)
|
||||
}
|
||||
|
||||
func (p *YesOrNoPrinter) Printf(format string, a ...interface{}) (int, error) {
|
||||
return fmt.Fprintf(p.out, format, a...)
|
||||
}
|
||||
|
||||
func (p *YesOrNoPrinter) Println(a ...interface{}) (int, error) {
|
||||
return fmt.Fprintln(p.out, a...)
|
||||
}
|
||||
|
||||
func (p *YesOrNoPrinter) PrintWithIndentf(format string, a ...interface{}) (int, error) {
|
||||
format = fmt.Sprintf("%s%s", p.indent, format)
|
||||
return fmt.Fprintf(p.out, format, a...)
|
||||
}
|
||||
|
||||
func (p *YesOrNoPrinter) PrintWithIndentln(a ...interface{}) (int, error) {
|
||||
n1, err := fmt.Fprintf(p.out, "%s", p.indent)
|
||||
if err != nil {
|
||||
return n1, err
|
||||
}
|
||||
n2, err := fmt.Fprintln(p.out, a...)
|
||||
if err != nil {
|
||||
return n1 + n2, err
|
||||
}
|
||||
return n1 + n2, nil
|
||||
}
|
||||
|
||||
func (p *YesOrNoPrinter) Yesf(format string, a ...interface{}) (int, error) {
|
||||
return p.yes.Fprintf(p.out, format, a...)
|
||||
}
|
||||
|
||||
func (p *YesOrNoPrinter) Yesln(a ...interface{}) (int, error) {
|
||||
return p.yes.Fprintln(p.out, a...)
|
||||
}
|
||||
|
||||
func (p *YesOrNoPrinter) YesWithIndentf(format string, a ...interface{}) (int, error) {
|
||||
format = fmt.Sprintf("%s%s", p.indent, format)
|
||||
return p.yes.Fprintf(p.out, format, a...)
|
||||
}
|
||||
|
||||
func (p *YesOrNoPrinter) YesWithIndentln(a ...interface{}) (int, error) {
|
||||
n1, err := p.yes.Fprintf(p.out, "%s", p.indent)
|
||||
if err != nil {
|
||||
return n1, err
|
||||
}
|
||||
n2, err := p.yes.Fprintln(p.out, a...)
|
||||
if err != nil {
|
||||
return n1 + n2, err
|
||||
}
|
||||
return n1 + n2, nil
|
||||
}
|
||||
|
||||
func (p *YesOrNoPrinter) Nof(format string, a ...interface{}) (int, error) {
|
||||
return p.no.Fprintf(p.out, format, a...)
|
||||
}
|
||||
|
||||
func (p *YesOrNoPrinter) Noln(a ...interface{}) (int, error) {
|
||||
return p.no.Fprintln(p.out, a...)
|
||||
}
|
||||
|
||||
func (p *YesOrNoPrinter) NoWithIndentf(format string, a ...interface{}) (int, error) {
|
||||
format = fmt.Sprintf("%s%s", p.indent, format)
|
||||
return p.no.Fprintf(p.out, format, a...)
|
||||
}
|
||||
|
||||
func (p *YesOrNoPrinter) NoWithIndentln(a ...interface{}) (int, error) {
|
||||
n1, err := p.no.Fprintf(p.out, "%s", p.indent)
|
||||
if err != nil {
|
||||
return n1, err
|
||||
}
|
||||
n2, err := p.no.Fprintln(p.out, a...)
|
||||
if err != nil {
|
||||
return n1 + n2, err
|
||||
}
|
||||
return n1 + n2, nil
|
||||
}
|
||||
|
||||
func (p *YesOrNoPrinter) Ident() string { return p.indent.String() }
|
||||
|
||||
func (p *YesOrNoPrinter) IncIdentRepeat() { p.indent.IncRepeat() }
|
||||
|
||||
func (p *YesOrNoPrinter) DecIndentRepeat() { p.indent.DecRepeat() }
|
||||
|
||||
func (p *YesOrNoPrinter) SetIdentRepeat(v int) { p.indent.SetRepeat(v) }
|
||||
|
||||
type Indent struct {
|
||||
format string
|
||||
repeat int
|
||||
}
|
||||
|
||||
func NewIndent(format string, repeat int) *Indent {
|
||||
return &Indent{
|
||||
format: format,
|
||||
repeat: repeat,
|
||||
}
|
||||
}
|
||||
|
||||
func (i *Indent) String() string {
|
||||
return strings.Repeat(i.format, i.repeat)
|
||||
}
|
||||
|
||||
func (i *Indent) IncRepeat() { i.repeat++ }
|
||||
|
||||
func (i *Indent) DecRepeat() {
|
||||
i.repeat--
|
||||
if i.repeat < 0 {
|
||||
i.repeat = 0
|
||||
}
|
||||
}
|
||||
|
||||
func (i *Indent) SetRepeat(v int) {
|
||||
if v < 0 {
|
||||
v = 0
|
||||
}
|
||||
i.repeat = v
|
||||
}
|
||||
31
pkg/cmd/hgctl/plugin/utils/survey_wrapper.go
Normal file
31
pkg/cmd/hgctl/plugin/utils/survey_wrapper.go
Normal file
@@ -0,0 +1,31 @@
|
||||
// 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 utils
|
||||
|
||||
import "github.com/AlecAivazis/survey/v2"
|
||||
|
||||
func Ask(qs []*survey.Question, response interface{}, opts ...survey.AskOpt) error {
|
||||
opts = append(opts, survey.WithIcons(func(set *survey.IconSet) {
|
||||
set.Error.Format = "red+hb"
|
||||
}))
|
||||
return survey.Ask(qs, response, opts...)
|
||||
}
|
||||
|
||||
func AskOne(p survey.Prompt, response interface{}, opts ...survey.AskOpt) error {
|
||||
opts = append(opts, survey.WithIcons(func(set *survey.IconSet) {
|
||||
set.Error.Format = "red+hb"
|
||||
}))
|
||||
return survey.AskOne(p, response, opts...)
|
||||
}
|
||||
@@ -14,7 +14,10 @@
|
||||
|
||||
package hgctl
|
||||
|
||||
import "github.com/spf13/cobra"
|
||||
import (
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/plugin"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// GetRootCommand returns the root cobra command to be executed
|
||||
// by hgctl main.
|
||||
@@ -34,6 +37,7 @@ func GetRootCommand() *cobra.Command {
|
||||
rootCmd.AddCommand(newProfileCmd())
|
||||
rootCmd.AddCommand(newDashboardCmd())
|
||||
rootCmd.AddCommand(newManifestCmd())
|
||||
rootCmd.AddCommand(plugin.NewCommand())
|
||||
|
||||
return rootCmd
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user