blob: 4e97f5756d67bfc3044f64f408f237d8f43102ec [file]
//
// Licensed to the Apache Software Foundation (ASF) under one or more
// contributor license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright ownership.
// The ASF licenses this file to You under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance with
// the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package validate
import (
"bufio"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"github.com/apache/dubbo-kubernetes/cli/pkg/cli"
operator "github.com/apache/dubbo-kubernetes/operator/pkg/apis"
operatorvalidate "github.com/apache/dubbo-kubernetes/operator/pkg/apis/validation"
"github.com/apache/dubbo-kubernetes/pkg/config/validation"
"github.com/apache/dubbo-kubernetes/pkg/slices"
"github.com/hashicorp/go-multierror"
"github.com/spf13/cobra"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/util/yaml"
)
var (
errFiles = errors.New(`error: you must specify resources by -f`)
validFields = map[string]struct{}{
"apiVersion": {},
"kind": {},
"metadata": {},
"spec": {},
"status": {},
}
fileExtensions = []string{".json", ".yaml", ".yml"}
)
type validator struct{}
func (v *validator) validateFile(path string, dubboNamespace *string, reader io.Reader, writer io.Writer) (validation.Warning, error) {
yamlReader := yaml.NewYAMLReader(bufio.NewReader(reader))
var errs error
var warnings validation.Warning
for {
doc, err := yamlReader.Read()
if err == io.EOF {
return warnings, errs
}
if err != nil {
errs = multierror.Append(errs, multierror.Prefix(err, fmt.Sprintf("failed to decode file %s: ", path)))
return warnings, errs
}
if len(doc) == 0 {
continue
}
out := map[string]any{}
if err := yaml.UnmarshalStrict(doc, &out); err != nil {
errs = multierror.Append(errs, multierror.Prefix(err, fmt.Sprintf("failed to decode file %s: ", path)))
return warnings, errs
}
un := unstructured.Unstructured{Object: out}
warning, err := v.validateResource(*dubboNamespace, &un, writer)
if err != nil {
errs = multierror.Append(errs, multierror.Prefix(err, fmt.Sprintf("%s/%s/%s:",
un.GetKind(), un.GetNamespace(), un.GetName())))
}
if warning != nil {
warnings = multierror.Append(warnings, multierror.Prefix(warning, fmt.Sprintf("%s/%s/%s:",
un.GetKind(), un.GetNamespace(), un.GetName())))
}
}
}
func (v *validator) validateResource(dubboNamespace string, un *unstructured.Unstructured, writer io.Writer) (validation.Warning, error) {
var errs error
if errs != nil {
return nil, errs
}
if un.GetAPIVersion() == operator.DubboOperatorGVK.GroupVersion().String() {
if un.GetKind() == operator.DubboOperatorGVK.Kind {
if err := checkFields(un); err != nil {
return nil, err
}
warnings, err := operatorvalidate.ParseAndValidateDubboOperator(un.Object, nil)
if err != nil {
return nil, err
}
if len(warnings) > 0 {
return validation.Warning(warnings.ToError()), nil
}
}
}
return nil, nil
}
func checkFields(un *unstructured.Unstructured) error {
var errs error
for key := range un.Object {
if _, ok := validFields[key]; !ok {
errs = multierror.Append(errs, fmt.Errorf("unknown field %q", key))
}
}
return errs
}
func NewValidateCommand(ctx cli.Context) *cobra.Command {
var files []string
vc := &cobra.Command{
Use: "validate -f FILENAME [options]",
Short: "Validate Dubbo rules files",
Long: "The validate command is used to validate the dubbo related rule file",
Example: ` # Validate current deployments under 'default' namespace with in the cluster
kubectl get deployments -o yaml | dubboctl validate -f -
# Validate current services under 'default' namespace with in the cluster
kubectl get services -o yaml | dubboctl validate -f -
# Validate resource yaml specifications
dubboctl validate -f default.yaml
`,
Args: cobra.NoArgs,
Aliases: []string{"v"},
RunE: func(cmd *cobra.Command, _ []string) error {
dn := ctx.Namespace()
return validateFiles(&dn, files, cmd.OutOrStderr())
},
}
flags := vc.PersistentFlags()
flags.StringSliceVarP(&files, "filename", "f", nil, "Inputs of files to validate")
return vc
}
func validateFiles(dubboNamespace *string, files []string, writer io.Writer) error {
if len(files) == 0 {
return errFiles
}
v := &validator{}
var errs error
var reader io.ReadCloser
warningsByFilename := map[string]validation.Warning{}
processFile := func(path string) {
var err error
if path == "-" {
reader = io.NopCloser(os.Stdin)
} else {
reader, err = os.Open(path)
if err != nil {
errs = multierror.Append(errs, fmt.Errorf("cannot read file %q: %v", path, err))
return
}
}
warning, err := v.validateFile(path, dubboNamespace, reader, writer)
if err != nil {
errs = multierror.Append(errs, err)
}
err = reader.Close()
if err != nil {
fmt.Printf("file: %s is not closed: %v", path, err)
}
warningsByFilename[path] = warning
}
processDirectory := func(directory string, processFile func(string)) error {
err := filepath.Walk(directory, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return nil
}
if isFileFormatValid(path) {
processFile(path)
}
return nil
})
return err
}
processedFiles := map[string]bool{}
for _, filename := range files {
var isDir bool
if filename != "-" {
fi, err := os.Stat(filename)
if err != nil {
errs = multierror.Append(errs, fmt.Errorf("cannot stat file %q: %v", filename, err))
continue
}
isDir = fi.IsDir()
}
if !isDir {
processFile(filename)
processedFiles[filename] = true
continue
}
if err := processDirectory(filename, func(path string) {
processFile(path)
processedFiles[path] = true
}); err != nil {
errs = multierror.Append(errs, err)
}
}
files = []string{}
for p := range processedFiles {
files = append(files, p)
}
if errs != nil {
for _, fname := range files {
if w := warningsByFilename[fname]; w != nil {
if fname == "-" {
_, _ = fmt.Fprint(writer, warningToString(w))
break
}
_, _ = fmt.Fprintf(writer, "%q has warnings: %v\n", fname, warningToString(w))
}
}
return errs
}
for _, fname := range files {
if fname == "-" {
if w := warningsByFilename[fname]; w != nil {
_, _ = fmt.Fprint(writer, warningToString(w))
} else {
_, _ = fmt.Fprintf(writer, "validation successfully completed\n")
}
break
}
if w := warningsByFilename[fname]; w != nil {
_, _ = fmt.Fprintf(writer, "%q has warnings: %v\n", fname, warningToString(w))
} else {
_, _ = fmt.Fprintf(writer, "%q is valid\n", fname)
}
}
return nil
}
func warningToString(w validation.Warning) string {
we, ok := w.(*multierror.Error)
if ok {
we.ErrorFormat = func(i []error) string {
points := make([]string, len(i))
for i, err := range i {
points[i] = fmt.Sprintf("* %s", err)
}
return fmt.Sprintf(
"\n\t%s\n",
strings.Join(points, "\n\t"))
}
}
return w.Error()
}
func isFileFormatValid(file string) bool {
ext := filepath.Ext(file)
return slices.Contains(fileExtensions, ext)
}