Skip to content

Commit ea5c5ea

Browse files
committed
feat(kumactl): allow apply of an entire directory
1 parent b4445b1 commit ea5c5ea

File tree

1 file changed

+129
-47
lines changed

1 file changed

+129
-47
lines changed

app/kumactl/cmd/apply/apply.go

+129-47
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

@@ -27,6 +29,8 @@ const (
2729
timeout = 10 * time.Second
2830
)
2931

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

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

6569
var b []byte
6670
var err error
71+
var resources []model.Resource
6772

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

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

0 commit comments

Comments
 (0)