@@ -3,26 +3,27 @@ package apply
33import (
44 "context"
55 "fmt"
6- "os"
76 "sort"
8- "strings"
97
108 "github.com/MakeNowJust/heredoc/v2"
11- "github.com/bmatcuk/doublestar/v4"
129 "github.com/charmbracelet/log"
1310 "github.com/ctrlplanedev/cli/internal/api"
11+ "github.com/ctrlplanedev/cli/internal/api/providers"
12+ "github.com/ctrlplanedev/cli/internal/api/resolver"
1413 "github.com/fatih/color"
14+ "github.com/google/uuid"
1515 "github.com/spf13/cobra"
1616 "github.com/spf13/viper"
1717)
1818
1919// NewApplyCmd creates a new apply command
2020func NewApplyCmd () * cobra.Command {
2121 var filePatterns []string
22+ var selectorRaw string
2223
2324 cmd := & cobra.Command {
2425 Use : "apply" ,
25- Short : "Apply a YAML configuration file to create or update resources " ,
26+ Short : "Apply a YAML configuration file using the new provider framework " ,
2627 Long : `Apply a YAML configuration file to create or update resources in Ctrlplane.` ,
2728 Example : heredoc .Doc (`
2829 # Apply a single resource file
@@ -34,96 +35,23 @@ func NewApplyCmd() *cobra.Command {
3435 # Apply all YAML files matching a glob pattern
3536 $ ctrlc apply -f "**/*.ctrlc.yaml"
3637
37- # Apply multiple patterns
38- $ ctrlc apply -f infra/*.yaml -f apps/*.yaml
39-
40- # Exclude test files using ! prefix (git-style: last match wins)
41- $ ctrlc apply -f "**/*.yaml" -f "!**/test*.yaml"
42-
43- # Exclude multiple patterns
44- $ ctrlc apply -f "**/*.yaml" -f "!**/test*.yaml" -f "!**/staging/**"
45-
46- # Re-include a previously excluded file (last pattern wins)
47- $ ctrlc apply -f "**/*.yaml" -f "!**/test*.yaml" -f "**/important-test.yaml"
38+ # Apply URL
39+ $ ctrlc apply -f https://example.com/config.yaml
4840 ` ),
4941 SilenceUsage : true ,
5042 RunE : func (cmd * cobra.Command , args []string ) error {
51- return runApply (cmd .Context (), filePatterns )
43+ return runApply (cmd .Context (), filePatterns , selectorRaw )
5244 },
5345 }
5446
5547 cmd .Flags ().StringArrayVarP (& filePatterns , "file" , "f" , nil , "Path or glob pattern to YAML files (can be specified multiple times, prefix with ! to exclude)" )
48+ cmd .Flags ().StringVar (& selectorRaw , "selector" , "" , "Metadata selector in key=value format to apply to created resources" )
5649 cmd .MarkFlagRequired ("file" )
5750
5851 return cmd
5952}
6053
61- // expandGlob expands glob patterns to file paths, supporting ** for recursive matching
62- // It follows git-style pattern matching where later patterns override earlier ones
63- // and ! prefix negates (excludes) a pattern
64- func expandGlob (patterns []string ) ([]string , error ) {
65- seen := make (map [string ]bool )
66- var files []string
67-
68- // Parse patterns into rules - ! prefix means exclude
69- type patternRule struct {
70- pattern string
71- include bool // true = include, false = exclude
72- }
73-
74- var rules []patternRule
75- for _ , p := range patterns {
76- if strings .HasPrefix (p , "!" ) {
77- rules = append (rules , patternRule {strings .TrimPrefix (p , "!" ), false })
78- } else {
79- rules = append (rules , patternRule {p , true })
80- }
81- }
82-
83- // First, collect all potential files from include patterns
84- candidateFiles := make (map [string ]bool )
85- for _ , rule := range rules {
86- if rule .include {
87- matches , err := doublestar .FilepathGlob (rule .pattern )
88- if err != nil {
89- return nil , fmt .Errorf ("invalid glob pattern '%s': %w" , rule .pattern , err )
90- }
91- for _ , match := range matches {
92- info , err := os .Stat (match )
93- if err != nil || info .IsDir () {
94- continue
95- }
96- candidateFiles [match ] = true
97- }
98- }
99- }
100-
101- // For each candidate file, evaluate all rules in order - last match wins
102- for filePath := range candidateFiles {
103- included := false
104- for _ , rule := range rules {
105- matched , err := doublestar .PathMatch (rule .pattern , filePath )
106- if err != nil {
107- return nil , fmt .Errorf ("invalid pattern '%s': %w" , rule .pattern , err )
108- }
109- if matched {
110- included = rule .include // last matching rule wins
111- }
112- }
113- if included && ! seen [filePath ] {
114- seen [filePath ] = true
115- files = append (files , filePath )
116- }
117- }
118-
119- if len (files ) == 0 {
120- return nil , fmt .Errorf ("no files matched patterns" )
121- }
122-
123- return files , nil
124- }
125-
126- func runApply (ctx context.Context , filePatterns []string ) error {
54+ func runApply (ctx context.Context , filePatterns []string , selectorRaw string ) error {
12755 files , err := expandGlob (filePatterns )
12856 if err != nil {
12957 return err
@@ -143,56 +71,41 @@ func runApply(ctx context.Context, filePatterns []string) error {
14371 }
14472
14573 workspaceID := client .GetWorkspaceID (ctx , workspace )
74+ if workspaceID == uuid .Nil {
75+ return fmt .Errorf ("workspace not found: %s" , workspace )
76+ }
77+
78+ resolver := resolver .NewAPIResolver (client , workspaceID )
79+ applyCtx := NewProviderContext (ctx , workspaceID .String (), client , resolver )
14680
147- var documents []Document
81+ var specs []providers. TypedSpec
14882 for _ , filePath := range files {
149- docs , err := ParseFile (filePath )
83+ fileSpecs , err := ParseFile (filePath )
15084 if err != nil {
15185 return fmt .Errorf ("failed to parse file %s: %w" , filePath , err )
15286 }
153- documents = append (documents , docs ... )
87+ specs = append (specs , fileSpecs ... )
15488 }
15589
156- if len (documents ) == 0 {
90+ if len (specs ) == 0 {
15791 log .Warn ("No resources found in files" )
15892 return nil
15993 }
16094
161- log .Info ("Applying resources" , "count" , len (documents ), "files" , len (files ))
162-
163- docCtx := NewDocContext (workspaceID .String (), client )
164- docCtx .Context = ctx
165-
166- var resourceDocs []* ResourceDocument
167- var otherDocs []Document
168- for _ , doc := range documents {
169- if rd , ok := doc .(* ResourceDocument ); ok {
170- resourceDocs = append (resourceDocs , rd )
171- } else {
172- otherDocs = append (otherDocs , doc )
173- }
174- }
175-
176- sort .Slice (otherDocs , func (i , j int ) bool {
177- return otherDocs [i ].Order () > otherDocs [j ].Order ()
178- })
179-
180- var results []ApplyResult
181- for _ , doc := range otherDocs {
182- result , err := doc .Apply (docCtx )
95+ if selectorRaw != "" {
96+ selector , err := providers .ParseSelector (selectorRaw )
18397 if err != nil {
184- log . Error ( "Failed to apply document" , "error" , err )
98+ return err
18599 }
186- results = append ( results , result )
100+ applySelectorToSpecs ( selector , specs )
187101 }
188102
189- if len (resourceDocs ) > 0 {
190- resourceResults , err := applyResourcesBatch (docCtx , resourceDocs )
191- if err != nil {
192- log .Error ("Failed to apply resources batch" , "error" , err )
193- }
194- results = append (results , resourceResults ... )
195- }
103+ log .Info ("Applying resources" , "count" , len (specs ), "files" , len (files ))
104+
105+ sortedSpecs := sortSpecsByOrder (specs )
106+ results := providers .
107+ DefaultProviderEngine .
108+ BatchApply (applyCtx , sortedSpecs , providers.BatchApplyOptions {})
196109
197110 printResults (results )
198111
@@ -205,17 +118,46 @@ func runApply(ctx context.Context, filePatterns []string) error {
205118 return nil
206119}
207120
208- func printResults (results []ApplyResult ) {
121+ func sortSpecsByOrder (specs []providers.TypedSpec ) []providers.TypedSpec {
122+ type orderedSpec struct {
123+ spec providers.TypedSpec
124+ order int
125+ index int
126+ }
127+
128+ ordered := make ([]orderedSpec , 0 , len (specs ))
129+ for idx , spec := range specs {
130+ order := 0
131+ if provider , ok := providers .DefaultProviderEngine .GetProvider (spec .Type ); ok {
132+ order = provider .Order ()
133+ }
134+ ordered = append (ordered , orderedSpec {spec : spec , order : order , index : idx })
135+ }
136+
137+ sort .Slice (ordered , func (i , j int ) bool {
138+ if ordered [i ].order == ordered [j ].order {
139+ return ordered [i ].index < ordered [j ].index
140+ }
141+ return ordered [i ].order > ordered [j ].order
142+ })
143+
144+ sorted := make ([]providers.TypedSpec , 0 , len (ordered ))
145+ for _ , item := range ordered {
146+ sorted = append (sorted , item .spec )
147+ }
148+
149+ return sorted
150+ }
151+
152+ func printResults (results []providers.Result ) {
209153 fmt .Println ()
210154
211- // Color definitions
212155 green := color .New (color .FgGreen , color .Bold )
213156 red := color .New (color .FgRed , color .Bold )
214157 cyan := color .New (color .FgCyan )
215158 yellow := color .New (color .FgYellow )
216159 dim := color .New (color .Faint )
217160
218- // Print apply results
219161 for _ , r := range results {
220162 if r .Error != nil {
221163 red .Print ("✗ " )
@@ -232,7 +174,6 @@ func printResults(results []ApplyResult) {
232174
233175 fmt .Println ()
234176
235- // Count successes and failures
236177 var success , failed int
237178 for _ , r := range results {
238179 if r .Error != nil {
@@ -242,7 +183,6 @@ func printResults(results []ApplyResult) {
242183 }
243184 }
244185
245- // Summary with colors
246186 fmt .Printf ("Applied %d resources: " , len (results ))
247187 green .Printf ("%d succeeded" , success )
248188 fmt .Print (", " )
0 commit comments