Skip to content

Commit

Permalink
feat(kumactl): allow apply of an entire directory
Browse files Browse the repository at this point in the history
  • Loading branch information
lplazas committed Dec 17, 2023
1 parent b4445b1 commit ea5c5ea
Showing 1 changed file with 129 additions and 47 deletions.
176 changes: 129 additions & 47 deletions app/kumactl/cmd/apply/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ package apply
import (
"context"
"fmt"
"golang.org/x/exp/slices"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"time"

Expand All @@ -27,6 +29,8 @@ const (
timeout = 10 * time.Second
)

var yamlExt = []string{".yaml", ".yml"}

type applyContext struct {
*kumactl_cmd.RootContext

Expand Down Expand Up @@ -64,66 +68,55 @@ $ kumactl apply -f https://example.com/resource.yaml

var b []byte
var err error
var resources []model.Resource

if ctx.args.file == "-" {
b, err = io.ReadAll(cmd.InOrStdin())
if err != nil {
return err
}
} else {
if strings.HasPrefix(ctx.args.file, "http://") || strings.HasPrefix(ctx.args.file, "https://") {
client := &http.Client{
Timeout: timeout,
}
req, err := http.NewRequest("GET", ctx.args.file, nil)
if err != nil {
return errors.Wrap(err, "error creating new http request")
}
resp, err := client.Do(req)
if err != nil {
return errors.Wrap(err, "error with GET http request")
}
if resp.StatusCode != http.StatusOK {
return errors.Wrap(err, "error while retrieving URL")
}
defer resp.Body.Close()
b, err = io.ReadAll(resp.Body)
if err != nil {
return errors.Wrap(err, "error while reading provided file")
}
} else {
b, err = os.ReadFile(ctx.args.file)
if err != nil {
return errors.Wrap(err, "error while reading provided file")
}
if len(b) == 0 {
return fmt.Errorf("no resource(s) passed to apply")
}
}
if len(b) == 0 {
return fmt.Errorf("no resource(s) passed to apply")
}
var resources []model.Resource
rawResources := yaml.SplitYAML(string(b))
for _, rawResource := range rawResources {
if len(rawResource) == 0 {
continue
r, err := bytesToResources(ctx, cmd, b)
if err != nil {
return errors.Wrap(err, "error parsing file to resources")
}
bytes := []byte(rawResource)
if len(ctx.args.vars) > 0 {
bytes = template.Render(rawResource, ctx.args.vars)
resources = append(resources, r...)
} else if strings.HasPrefix(ctx.args.file, "http://") || strings.HasPrefix(ctx.args.file, "https://") {
client := &http.Client{
Timeout: timeout,
}
res, err := rest_types.YAML.UnmarshalCore(bytes)
req, err := http.NewRequest("GET", ctx.args.file, nil)
if err != nil {
return errors.Wrap(err, "YAML contains invalid resource")
return errors.Wrap(err, "error creating new http request")
}
if err, msg := mesh.ValidateMetaBackwardsCompatible(res.GetMeta(), res.Descriptor().Scope); err.HasViolations() {
return err.OrNil()
} else if msg != "" {
if _, printErr := fmt.Fprintln(cmd.ErrOrStderr(), msg); printErr != nil {
return printErr
}
resp, err := client.Do(req)
if err != nil {
return errors.Wrap(err, "error with GET http request")
}
if resp.StatusCode != http.StatusOK {
return errors.Wrap(err, "error while retrieving URL")
}
defer resp.Body.Close()
b, err = io.ReadAll(resp.Body)
if err != nil {
return errors.Wrap(err, "error while reading provided file")
}
r, err := bytesToResources(ctx, cmd, b)
if err != nil {
return errors.Wrap(err, "error parsing file to resources")
}
resources = append(resources, res)
resources = append(resources, r...)
} else {
// Process local yaml files
r, err := localFileToResources(ctx, cmd)
if err != nil {
return errors.Wrap(err, "error processing file")
}
resources = append(resources, r...)
}

var rs store.ResourceStore
if !ctx.args.dryRun {
rs, err = pctx.CurrentResourceStore()
Expand Down Expand Up @@ -153,6 +146,95 @@ $ kumactl apply -f https://example.com/resource.yaml
return cmd
}

// localFileToResources reads and converts a local file into a list of model.Resource
// the local file could be a directory, in which case it processes all the yaml files in the directory
func localFileToResources(ctx *applyContext, cmd *cobra.Command) ([]model.Resource, error) {
var resources []model.Resource
file, err := os.Open(ctx.args.file)
if err != nil {
return nil, errors.Wrap(err, "error while opening provided file")
}
defer file.Close()
orgDir, _ := filepath.Split(ctx.args.file)

fileInfo, err := file.Stat()
if err != nil {
return nil, errors.Wrap(err, "error getting stats for the provided file")
}

var yamlFiles []string
if fileInfo.IsDir() {
for {
names, err := file.Readdirnames(10)
if err != nil {
if err == io.EOF {
break
} else {
return nil, errors.Wrap(err, "error reading file names in directory")
}
}
for _, n := range names {
if slices.Contains(yamlExt, filepath.Ext(n)) {
yamlFiles = append(yamlFiles, n)
}
}
}
} else {
if slices.Contains(yamlExt, filepath.Ext(fileInfo.Name())) {
yamlFiles = append(yamlFiles, fileInfo.Name())
}
// TODO should this check be added?
//else {
// return nil, fmt.Errorf("error the specified input file extension isn't yaml")
//}
}
var b []byte
for _, f := range yamlFiles {
joined := filepath.Join(orgDir, f)
b, err = os.ReadFile(joined)
if err != nil {
return nil, errors.Wrap(err, fmt.Sprintf("error while reading the provided file [%s]", f))
}
r, err := bytesToResources(ctx, cmd, b)
if err != nil {
return nil, errors.Wrap(err, fmt.Sprintf("error parsing file [%s] to resources", f))
}
resources = append(resources, r...)
}
if len(resources) == 0 {
return nil, fmt.Errorf("no resource(s) passed to apply")
}
return resources, nil
}

// bytesToResources converts a slice of bytes into a slice of model.Resource
func bytesToResources(ctx *applyContext, cmd *cobra.Command, fileBytes []byte) ([]model.Resource, error) {
var resources []model.Resource
rawResources := yaml.SplitYAML(string(fileBytes))
for _, rawResource := range rawResources {
if len(rawResource) == 0 {
continue
}
bytes := []byte(rawResource)
if len(ctx.args.vars) > 0 {
bytes = template.Render(rawResource, ctx.args.vars)
}
res, err := rest_types.YAML.UnmarshalCore(bytes)
if err != nil {
return nil, errors.Wrap(err, "YAML contains invalid resource")
}
if err, msg := mesh.ValidateMetaBackwardsCompatible(res.GetMeta(), res.Descriptor().Scope); err.HasViolations() {
return nil, err.OrNil()
} else if msg != "" {
if _, printErr := fmt.Fprintln(cmd.ErrOrStderr(), msg); printErr != nil {
return nil, printErr
}
}
resources = append(resources, res)
}
return resources, nil
}

func upsert(ctx context.Context, typeRegistry registry.TypeRegistry, rs store.ResourceStore, res model.Resource) error {
newRes, err := typeRegistry.NewObject(res.Descriptor().Name)
if err != nil {
Expand Down

0 comments on commit ea5c5ea

Please sign in to comment.