Skip to content

Commit 435f287

Browse files
committed
[cli] expose resource.Apply/ReadInputs/SplitYAMLDocuments
Promote the apply machinery out of pkg/cmd into pkg/cmd/resource so external callers (clrk, future tools) can server-side apply CRD YAMLs without importing the cobra command package. Decoding now goes through sigs.k8s.io/yaml directly into unstructured.Unstructured so callers don't have to register their CRDs against apoxy-cli's scheme — clrk's clrk.apoxy.dev types apply out of the box. The apoxy apply/delete subcommands are refactored onto the same helpers; behavior is unchanged.
1 parent 162899b commit 435f287

3 files changed

Lines changed: 194 additions & 173 deletions

File tree

pkg/cmd/apply.go

Lines changed: 12 additions & 159 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,15 @@
11
package cmd
22

33
import (
4-
"context"
54
"fmt"
6-
"io"
75
"os"
8-
"path/filepath"
9-
"strings"
106

117
"github.com/spf13/cobra"
12-
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
13-
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
14-
"k8s.io/apimachinery/pkg/runtime"
15-
"k8s.io/apimachinery/pkg/types"
168
"k8s.io/client-go/discovery"
179
memory "k8s.io/client-go/discovery/cached"
1810
"k8s.io/client-go/dynamic"
1911
"k8s.io/client-go/restmapper"
2012

21-
"github.com/apoxy-dev/apoxy/client/versioned/scheme"
2213
"github.com/apoxy-dev/apoxy/config"
2314
"github.com/apoxy-dev/apoxy/pkg/cmd/resource"
2415
)
@@ -63,7 +54,6 @@ Examples:
6354
return err
6455
}
6556

66-
// Set up dynamic client and REST mapper.
6757
dc, err := discovery.NewDiscoveryClientForConfig(c.RESTConfig)
6858
if err != nil {
6959
return fmt.Errorf("failed to create discovery client: %w", err)
@@ -74,178 +64,41 @@ Examples:
7464
}
7565
mapper := restmapper.NewDeferredDiscoveryRESTMapper(memory.NewMemCacheClient(dc))
7666

77-
// Collect all file contents.
78-
var allData [][]byte
79-
for _, f := range applyFiles {
80-
data, err := readInput(f, applyRecursive)
81-
if err != nil {
82-
return err
83-
}
84-
allData = append(allData, data...)
67+
allData, err := resource.ReadInputs(applyFiles, applyRecursive)
68+
if err != nil {
69+
return err
8570
}
8671

72+
opts := resource.ApplyOptions{
73+
FieldManager: applyFieldManager,
74+
Force: applyForceConflicts,
75+
}
8776
var errs []error
8877
var applied int
89-
9078
for _, data := range allData {
91-
docs := splitYAMLDocuments(data)
92-
for _, doc := range docs {
93-
if len(strings.TrimSpace(string(doc))) == 0 {
94-
continue
95-
}
96-
97-
name, kind, err := applyResource(cmd.Context(), dynClient, mapper, doc)
79+
for _, doc := range resource.SplitYAMLDocuments(data) {
80+
name, kind, err := resource.Apply(cmd.Context(), dynClient, mapper, doc, opts)
9881
if err != nil {
9982
errs = append(errs, err)
10083
fmt.Fprintf(os.Stderr, "error: %v\n", err)
101-
} else {
102-
fmt.Printf("%s %q applied\n", strings.ToLower(kind), name)
103-
applied++
84+
continue
10485
}
86+
fmt.Printf("%s %q applied\n", kind, name)
87+
applied++
10588
}
10689
}
10790

10891
if len(errs) > 0 {
10992
fmt.Fprintf(os.Stderr, "\nApplied %d resource(s) with %d error(s)\n", applied, len(errs))
11093
return fmt.Errorf("failed to apply %d resource(s)", len(errs))
11194
}
112-
11395
if applied > 1 {
11496
fmt.Printf("\nApplied %d resources\n", applied)
11597
}
11698
return nil
11799
},
118100
}
119101

120-
// readInput reads content from a file, directory, or stdin.
121-
func readInput(path string, recursive bool) ([][]byte, error) {
122-
if path == "-" {
123-
data, err := io.ReadAll(os.Stdin)
124-
if err != nil {
125-
return nil, fmt.Errorf("failed to read stdin: %w", err)
126-
}
127-
return [][]byte{data}, nil
128-
}
129-
130-
info, err := os.Stat(path)
131-
if err != nil {
132-
return nil, fmt.Errorf("failed to stat %s: %w", path, err)
133-
}
134-
135-
if info.IsDir() {
136-
return readDirectory(path, recursive)
137-
}
138-
139-
data, err := os.ReadFile(path)
140-
if err != nil {
141-
return nil, fmt.Errorf("failed to read %s: %w", path, err)
142-
}
143-
return [][]byte{data}, nil
144-
}
145-
146-
// readDirectory reads all YAML/JSON files from a directory.
147-
func readDirectory(dir string, recursive bool) ([][]byte, error) {
148-
var result [][]byte
149-
150-
walkFn := func(path string, info os.FileInfo, err error) error {
151-
if err != nil {
152-
return err
153-
}
154-
155-
// Skip directories (but continue into them if recursive)
156-
if info.IsDir() {
157-
if path != dir && !recursive {
158-
return filepath.SkipDir
159-
}
160-
return nil
161-
}
162-
163-
// Only process YAML and JSON files
164-
ext := strings.ToLower(filepath.Ext(path))
165-
if ext != ".yaml" && ext != ".yml" && ext != ".json" {
166-
return nil
167-
}
168-
169-
data, err := os.ReadFile(path)
170-
if err != nil {
171-
return fmt.Errorf("failed to read %s: %w", path, err)
172-
}
173-
result = append(result, data)
174-
return nil
175-
}
176-
177-
if err := filepath.Walk(dir, walkFn); err != nil {
178-
return nil, err
179-
}
180-
181-
return result, nil
182-
}
183-
184-
// splitYAMLDocuments splits a YAML file into multiple documents.
185-
func splitYAMLDocuments(data []byte) [][]byte {
186-
var docs [][]byte
187-
for _, doc := range strings.Split(string(data), "\n---") {
188-
docs = append(docs, []byte(doc))
189-
}
190-
return docs
191-
}
192-
193-
// applyResource decodes and applies a single resource using the dynamic client.
194-
func applyResource(
195-
ctx context.Context,
196-
dynClient dynamic.Interface,
197-
mapper *restmapper.DeferredDiscoveryRESTMapper,
198-
data []byte,
199-
) (name, kind string, err error) {
200-
// Decode into unstructured object.
201-
unObj := &unstructured.Unstructured{}
202-
_, gvk, err := scheme.Codecs.UniversalDeserializer().Decode(data, nil, unObj)
203-
if err != nil {
204-
return "", "", fmt.Errorf("failed to decode resource: %w", err)
205-
}
206-
207-
name = unObj.GetName()
208-
if name == "" {
209-
if fn, ok := resource.LookupDefaultName(*gvk); ok {
210-
derivedName, err := fn(data)
211-
if err != nil {
212-
return "", "", err
213-
}
214-
unObj.SetName(derivedName)
215-
name = derivedName
216-
}
217-
}
218-
if name == "" {
219-
return "", "", fmt.Errorf("resource name is required")
220-
}
221-
kind = gvk.Kind
222-
223-
// Get the REST mapping for this GVK.
224-
mapping, err := mapper.RESTMapping(gvk.GroupKind(), gvk.Version)
225-
if err != nil {
226-
return name, kind, fmt.Errorf("failed to get REST mapping for %s: %w", gvk.String(), err)
227-
}
228-
229-
// Encode for patch.
230-
patchData, err := runtime.Encode(unstructured.UnstructuredJSONScheme, unObj)
231-
if err != nil {
232-
return name, kind, fmt.Errorf("failed to encode resource: %w", err)
233-
}
234-
235-
// Apply using server-side apply.
236-
_, err = dynClient.Resource(mapping.Resource).
237-
Namespace(unObj.GetNamespace()).
238-
Patch(ctx, name, types.ApplyPatchType, patchData, metav1.PatchOptions{
239-
FieldManager: applyFieldManager,
240-
Force: &applyForceConflicts,
241-
})
242-
if err != nil {
243-
return name, kind, fmt.Errorf("failed to apply %s %q: %w", kind, name, err)
244-
}
245-
246-
return name, kind, nil
247-
}
248-
249102
func init() {
250103
applyCmd.Flags().StringArrayVarP(&applyFiles, "filename", "f", nil,
251104
"Files or directories containing resources to apply (can be specified multiple times)")

pkg/cmd/delete.go

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -109,26 +109,16 @@ func deleteFromFiles(
109109
dynClient dynamic.Interface,
110110
mapper *restmapper.DeferredDiscoveryRESTMapper,
111111
) error {
112-
// Collect all file contents.
113-
var allData [][]byte
114-
for _, f := range deleteFiles {
115-
data, err := readInput(f, deleteRecursive)
116-
if err != nil {
117-
return err
118-
}
119-
allData = append(allData, data...)
112+
allData, err := resource.ReadInputs(deleteFiles, deleteRecursive)
113+
if err != nil {
114+
return err
120115
}
121116

122117
var errs []error
123118
var deleted int
124119

125120
for _, data := range allData {
126-
docs := splitYAMLDocuments(data)
127-
for _, doc := range docs {
128-
if len(strings.TrimSpace(string(doc))) == 0 {
129-
continue
130-
}
131-
121+
for _, doc := range resource.SplitYAMLDocuments(data) {
132122
name, kind, notFound, err := deleteResource(ctx, dynClient, mapper, doc)
133123
if err != nil {
134124
errs = append(errs, err)

0 commit comments

Comments
 (0)