Skip to content

Commit 4b757b4

Browse files
committed
feat(kumactl): allow apply of an entire directory
1 parent 47a08c7 commit 4b757b4

File tree

1 file changed

+130
-48
lines changed

1 file changed

+130
-48
lines changed

app/kumactl/cmd/apply/apply.go

+130-48
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@ package apply
33
import (
44
"context"
55
"fmt"
6+
"golang.org/x/exp/slices"
67
"io"
78
"net/http"
89
"os"
10+
"path/filepath"
911
"strings"
1012
"time"
1113

@@ -28,6 +30,8 @@ const (
2830
timeout = 10 * time.Second
2931
)
3032

33+
var yamlExt = []string{".yaml", ".yml"}
34+
3135
type applyContext struct {
3236
*kumactl_cmd.RootContext
3337

@@ -63,66 +67,55 @@ $ kumactl apply -f https://example.com/resource.yaml
6367

6468
var b []byte
6569
var err error
70+
var resources []model.Resource
6671

6772
if ctx.args.file == "-" {
6873
b, err = io.ReadAll(cmd.InOrStdin())
6974
if err != nil {
7075
return err
7176
}
72-
} else {
73-
if strings.HasPrefix(ctx.args.file, "http://") || strings.HasPrefix(ctx.args.file, "https://") {
74-
client := &http.Client{
75-
Timeout: timeout,
76-
}
77-
req, err := http.NewRequest("GET", ctx.args.file, nil)
78-
if err != nil {
79-
return errors.Wrap(err, "error creating new http request")
80-
}
81-
resp, err := client.Do(req)
82-
if err != nil {
83-
return errors.Wrap(err, "error with GET http request")
84-
}
85-
if resp.StatusCode != http.StatusOK {
86-
return errors.Wrap(err, "error while retrieving URL")
87-
}
88-
defer resp.Body.Close()
89-
b, err = io.ReadAll(resp.Body)
90-
if err != nil {
91-
return errors.Wrap(err, "error while reading provided file")
92-
}
93-
} else {
94-
b, err = os.ReadFile(ctx.args.file)
95-
if err != nil {
96-
return errors.Wrap(err, "error while reading provided file")
97-
}
77+
if len(b) == 0 {
78+
return fmt.Errorf("no resource(s) passed to apply")
9879
}
99-
}
100-
if len(b) == 0 {
101-
return fmt.Errorf("no resource(s) passed to apply")
102-
}
103-
var resources []model.Resource
104-
rawResources := yaml.SplitYAML(string(b))
105-
for _, rawResource := range rawResources {
106-
if len(rawResource) == 0 {
107-
continue
80+
r, err := bytesToResources(ctx, cmd, b)
81+
if err != nil {
82+
return errors.Wrap(err, "error parsing file to resources")
10883
}
109-
bytes := []byte(rawResource)
110-
if len(ctx.args.vars) > 0 {
111-
bytes = template.Render(rawResource, ctx.args.vars)
84+
resources = append(resources, r...)
85+
} else if strings.HasPrefix(ctx.args.file, "http://") || strings.HasPrefix(ctx.args.file, "https://") {
86+
client := &http.Client{
87+
Timeout: timeout,
11288
}
113-
res, err := rest_types.YAML.UnmarshalCore(bytes)
89+
req, err := http.NewRequest("GET", ctx.args.file, nil)
11490
if err != nil {
115-
return errors.Wrap(err, "YAML contains invalid resource")
91+
return errors.Wrap(err, "error creating new http request")
11692
}
117-
if err, msg := mesh.ValidateMetaBackwardsCompatible(res.GetMeta(), res.Descriptor().Scope); err.HasViolations() {
118-
return err.OrNil()
119-
} else if msg != "" {
120-
if _, printErr := fmt.Fprintln(cmd.ErrOrStderr(), msg); printErr != nil {
121-
return printErr
122-
}
93+
resp, err := client.Do(req)
94+
if err != nil {
95+
return errors.Wrap(err, "error with GET http request")
96+
}
97+
if resp.StatusCode != http.StatusOK {
98+
return errors.Wrap(err, "error while retrieving URL")
99+
}
100+
defer resp.Body.Close()
101+
b, err = io.ReadAll(resp.Body)
102+
if err != nil {
103+
return errors.Wrap(err, "error while reading provided file")
104+
}
105+
r, err := bytesToResources(ctx, cmd, b)
106+
if err != nil {
107+
return errors.Wrap(err, "error parsing file to resources")
123108
}
124-
resources = append(resources, res)
109+
resources = append(resources, r...)
110+
} else {
111+
// Process local yaml files
112+
r, err := localFileToResources(ctx, cmd)
113+
if err != nil {
114+
return errors.Wrap(err, "error processing file")
115+
}
116+
resources = append(resources, r...)
125117
}
118+
126119
var rs store.ResourceStore
127120
if !ctx.args.dryRun {
128121
rs, err = pctx.CurrentResourceStore()
@@ -158,7 +151,96 @@ $ kumactl apply -f https://example.com/resource.yaml
158151
return cmd
159152
}
160153

161-
func upsert(ctx context.Context, typeRegistry registry.TypeRegistry, rs store.ResourceStore, res model.Resource) ([]string, error) {
154+
// localFileToResources reads and converts a local file into a list of model.Resource
155+
// the local file could be a directory, in which case it processes all the yaml files in the directory
156+
func localFileToResources(ctx *applyContext, cmd *cobra.Command) ([]model.Resource, error) {
157+
var resources []model.Resource
158+
file, err := os.Open(ctx.args.file)
159+
if err != nil {
160+
return nil, errors.Wrap(err, "error while opening provided file")
161+
}
162+
defer file.Close()
163+
orgDir, _ := filepath.Split(ctx.args.file)
164+
165+
fileInfo, err := file.Stat()
166+
if err != nil {
167+
return nil, errors.Wrap(err, "error getting stats for the provided file")
168+
}
169+
170+
var yamlFiles []string
171+
if fileInfo.IsDir() {
172+
for {
173+
names, err := file.Readdirnames(10)
174+
if err != nil {
175+
if err == io.EOF {
176+
break
177+
} else {
178+
return nil, errors.Wrap(err, "error reading file names in directory")
179+
}
180+
}
181+
for _, n := range names {
182+
if slices.Contains(yamlExt, filepath.Ext(n)) {
183+
yamlFiles = append(yamlFiles, n)
184+
}
185+
}
186+
}
187+
} else {
188+
if slices.Contains(yamlExt, filepath.Ext(fileInfo.Name())) {
189+
yamlFiles = append(yamlFiles, fileInfo.Name())
190+
}
191+
// TODO should this check be added?
192+
//else {
193+
// return nil, fmt.Errorf("error the specified input file extension isn't yaml")
194+
//}
195+
}
196+
var b []byte
197+
for _, f := range yamlFiles {
198+
joined := filepath.Join(orgDir, f)
199+
b, err = os.ReadFile(joined)
200+
if err != nil {
201+
return nil, errors.Wrap(err, fmt.Sprintf("error while reading the provided file [%s]", f))
202+
}
203+
r, err := bytesToResources(ctx, cmd, b)
204+
if err != nil {
205+
return nil, errors.Wrap(err, fmt.Sprintf("error parsing file [%s] to resources", f))
206+
}
207+
resources = append(resources, r...)
208+
}
209+
if len(resources) == 0 {
210+
return nil, fmt.Errorf("no resource(s) passed to apply")
211+
}
212+
return resources, nil
213+
}
214+
215+
// bytesToResources converts a slice of bytes into a slice of model.Resource
216+
func bytesToResources(ctx *applyContext, cmd *cobra.Command, fileBytes []byte) ([]model.Resource, error) {
217+
var resources []model.Resource
218+
rawResources := yaml.SplitYAML(string(fileBytes))
219+
for _, rawResource := range rawResources {
220+
if len(rawResource) == 0 {
221+
continue
222+
}
223+
bytes := []byte(rawResource)
224+
if len(ctx.args.vars) > 0 {
225+
bytes = template.Render(rawResource, ctx.args.vars)
226+
}
227+
res, err := rest_types.YAML.UnmarshalCore(bytes)
228+
if err != nil {
229+
return nil, errors.Wrap(err, "YAML contains invalid resource")
230+
}
231+
if err, msg := mesh.ValidateMetaBackwardsCompatible(res.GetMeta(), res.Descriptor().Scope); err.HasViolations() {
232+
return nil, err.OrNil()
233+
} else if msg != "" {
234+
if _, printErr := fmt.Fprintln(cmd.ErrOrStderr(), msg); printErr != nil {
235+
return nil, printErr
236+
}
237+
}
238+
resources = append(resources, res)
239+
}
240+
return resources, nil
241+
}
242+
243+
func upsert(ctx context.Context, typeRegistry registry.TypeRegistry, rs store.ResourceStore, res model.Resource) error {
162244
newRes, err := typeRegistry.NewObject(res.Descriptor().Name)
163245
if err != nil {
164246
return nil, err

0 commit comments

Comments
 (0)