karmada/pkg/karmadactl/util/genericresource/builder.go

185 lines
5.1 KiB
Go

/*
Copyright 2022 The Karmada Authors.
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 genericresource
import (
"errors"
"fmt"
"net/url"
"os"
"path/filepath"
"strings"
utilerrors "k8s.io/apimachinery/pkg/util/errors"
"k8s.io/cli-runtime/pkg/resource"
)
const defaultHTTPGetAttempts int = 3
var defaultNewFunc = func() interface{} {
return map[string]interface{}{}
}
var errMissingResource = fmt.Errorf(`you must provide one or more resources`)
// Builder provides convenience functions for taking arguments and parameters
// from the command line and converting them to a list of resources to iterate
// over using the Visitor interface.
type Builder struct {
errs []error
paths []Visitor
stdinInUse bool
mapper *mapper
schema resource.ContentValidator
}
// NewBuilder returns a Builder.
func NewBuilder() *Builder {
return &Builder{
mapper: &mapper{
newFunc: defaultNewFunc,
},
}
}
// Schema set the schema to validate data in files.
func (b *Builder) Schema(schema resource.ContentValidator) *Builder {
b.schema = schema
return b
}
// Constructor tells wanted type of object.
func (b *Builder) Constructor(newFunc func() interface{}) *Builder {
b.mapper.newFunc = newFunc
return b
}
// Filename groups input in two categories: URLs and files (files, directories, STDIN)
func (b *Builder) Filename(recursive bool, filenames ...string) *Builder {
for _, s := range filenames {
switch {
case s == "-":
b.Stdin()
case strings.HasPrefix(s, "http://") || strings.HasPrefix(s, "https://"):
u, err := url.Parse(s)
if err != nil {
b.errs = append(b.errs, fmt.Errorf("the URL passed to filename %q is not valid: %v", s, err))
continue
}
b.URL(defaultHTTPGetAttempts, u)
default:
matches, err := expandIfFilePattern(s)
if err != nil {
b.errs = append(b.errs, err)
continue
}
b.Path(recursive, matches...)
}
}
return b
}
// Stdin will read objects from the standard input.
func (b *Builder) Stdin() *Builder {
if b.stdinInUse {
b.errs = append(b.errs, resource.StdinMultiUseError)
}
b.stdinInUse = true
b.paths = append(b.paths, FileVisitorForSTDIN(b.mapper, b.schema))
return b
}
// URL accepts a number of URLs directly.
func (b *Builder) URL(httpAttemptCount int, urls ...*url.URL) *Builder {
for _, u := range urls {
b.paths = append(b.paths, NewURLVisitor(b.mapper, httpAttemptCount, u, b.schema))
}
return b
}
// Path accepts a set of paths that may be files, directories (all can contain
// one or more resources). Creates a FileVisitor for each file and then each
// FileVisitor is streaming the content to a StreamVisitor.
func (b *Builder) Path(recursive bool, paths ...string) *Builder {
for _, p := range paths {
_, err := os.Stat(p)
if os.IsNotExist(err) {
b.errs = append(b.errs, fmt.Errorf("the path %q does not exist", p))
continue
}
if err != nil {
b.errs = append(b.errs, fmt.Errorf("the path %q cannot be accessed: %v", p, err))
continue
}
visitors, err := ExpandPathsToFileVisitors(b.mapper, p, recursive, resource.FileExtensions, b.schema)
if err != nil {
b.errs = append(b.errs, fmt.Errorf("error reading %q: %v", p, err))
}
b.paths = append(b.paths, visitors...)
}
if len(b.paths) == 0 && len(b.errs) == 0 {
b.errs = append(b.errs, fmt.Errorf("error reading %v: recognized file extensions are %v", paths, resource.FileExtensions))
}
return b
}
// Do returns a Result object with a Visitor for the resources identified by the Builder. Note that stream
// inputs are consumed by the first execution - use Infos() or Objects() on the Result to capture a list
// for further iteration.
func (b *Builder) Do() *Result {
r := b.visitorResult()
return r
}
func (b *Builder) visitorResult() *Result {
if len(b.errs) > 0 {
return &Result{err: utilerrors.NewAggregate(b.errs)}
}
// visit items specified by paths
if len(b.paths) != 0 {
return b.visitByPaths()
}
return &Result{err: errMissingResource}
}
func (b *Builder) visitByPaths() *Result {
result := &Result{}
result.visitor = VisitorList(b.paths)
return result
}
// expandIfFilePattern returns all the filenames that match the input pattern
// or the filename if it is a specific filename and not a pattern.
// If the input is a pattern and it yields no result it will result in an error.
func expandIfFilePattern(pattern string) ([]string, error) {
if _, err := os.Stat(pattern); os.IsNotExist(err) {
matches, err := filepath.Glob(pattern)
if err == nil && len(matches) == 0 {
return nil, fmt.Errorf("the path %q does not exist", pattern)
}
if errors.Is(err, filepath.ErrBadPattern) {
return nil, fmt.Errorf("pattern %q is not valid: %v", pattern, err)
}
return matches, err
}
return []string{pattern}, nil
}