@@ -7,8 +7,10 @@ import (
77 "io"
88 "os"
99 "strings"
10+ "time"
1011
1112 "github.com/MakeNowJust/heredoc/v2"
13+ "github.com/avast/retry-go"
1214 "github.com/charmbracelet/log"
1315 "github.com/ctrlplanedev/cli/internal/api"
1416 "github.com/ctrlplanedev/cli/internal/cliutil"
@@ -35,6 +37,10 @@ func NewSyncPipeCmd() *cobra.Command {
3537 $ echo '{"name":"web-1","identifier":"web-1-prod","version":"custom/v1","kind":"Server"}' \
3638 | ctrlc sync pipe --provider "my-servers"
3739
40+ # Resource with variables
41+ $ echo '[{"name":"web-1","identifier":"web-1-prod","version":"custom/v1","kind":"Server","variables":{"env":"prod","tier":"frontend"}}]' \
42+ | ctrlc sync pipe --provider "my-servers"
43+
3844 # From curl with jq transformation
3945 $ curl -s https://cmdb.internal/api/servers \
4046 | jq '[.[] | {name, identifier: .id, version: "cmdb/v1", kind: "Server", config: ., metadata: {}}]' \
@@ -60,16 +66,17 @@ func NewSyncPipeCmd() *cobra.Command {
6066 }
6167
6268 // Parse JSON -- try array first, then single object
63- resources , err := parseResources (data )
69+ resourceInputs , err := parseResources (data )
6470 if err != nil {
6571 return err
6672 }
6773
6874 // Validate required fields
69- if err := validateResources (resources ); err != nil {
75+ if err := validateResources (resourceInputs ); err != nil {
7076 return err
7177 }
7278
79+ resources := toAPIResources (resourceInputs )
7380 log .Info ("Syncing resources from stdin" , "count" , len (resources ), "provider" , providerName )
7481
7582 // Create API client
@@ -94,6 +101,11 @@ func NewSyncPipeCmd() *cobra.Command {
94101 return fmt .Errorf ("failed to upsert resources: %w" , err )
95102 }
96103
104+ workspaceID := ctrlplaneClient .GetWorkspaceID (ctx , workspace ).String ()
105+ if err := syncResourceVariables (ctx , ctrlplaneClient , workspaceID , resourceInputs ); err != nil {
106+ return err
107+ }
108+
97109 log .Info ("Response from upserting resources" , "status" , upsertResp .Status )
98110
99111 return cliutil .HandleResponseOutput (cmd , upsertResp )
@@ -106,20 +118,79 @@ func NewSyncPipeCmd() *cobra.Command {
106118 return cmd
107119}
108120
121+ type resourceInput struct {
122+ Name string `json:"name"`
123+ Identifier string `json:"identifier"`
124+ Version string `json:"version"`
125+ Kind string `json:"kind"`
126+ Config map [string ]any `json:"config"`
127+ Metadata map [string ]string `json:"metadata"`
128+ Variables map [string ]any `json:"-"`
129+
130+ hasVariables bool `json:"-"`
131+ }
132+
133+ func (r * resourceInput ) UnmarshalJSON (data []byte ) error {
134+ type resourceInputAlias struct {
135+ Name string `json:"name"`
136+ Identifier string `json:"identifier"`
137+ Version string `json:"version"`
138+ Kind string `json:"kind"`
139+ Config map [string ]interface {} `json:"config"`
140+ Metadata map [string ]string `json:"metadata"`
141+ }
142+
143+ var alias resourceInputAlias
144+ if err := json .Unmarshal (data , & alias ); err != nil {
145+ return err
146+ }
147+
148+ var raw map [string ]json.RawMessage
149+ if err := json .Unmarshal (data , & raw ); err != nil {
150+ return err
151+ }
152+
153+ r .Name = alias .Name
154+ r .Identifier = alias .Identifier
155+ r .Version = alias .Version
156+ r .Kind = alias .Kind
157+ r .Config = alias .Config
158+ r .Metadata = alias .Metadata
159+
160+ varRaw , ok := raw ["variables" ]
161+ if ! ok {
162+ r .hasVariables = false
163+ return nil
164+ }
165+
166+ r .hasVariables = true
167+ if string (varRaw ) == "null" {
168+ r .Variables = map [string ]any {}
169+ return nil
170+ }
171+
172+ var vars map [string ]any
173+ if err := json .Unmarshal (varRaw , & vars ); err != nil {
174+ return fmt .Errorf ("invalid variables field: must be a JSON object" )
175+ }
176+ r .Variables = vars
177+ return nil
178+ }
179+
109180// parseResources attempts to parse the raw JSON data as either an array of
110181// resources or a single resource object. A single object is normalized to a
111182// one-element array.
112- func parseResources (data []byte ) ([]api. ResourceProviderResource , error ) {
183+ func parseResources (data []byte ) ([]resourceInput , error ) {
113184 // Try array first
114- var resources []api. ResourceProviderResource
185+ var resources []resourceInput
115186 if err := json .Unmarshal (data , & resources ); err == nil {
116187 return resources , nil
117188 }
118189
119190 // Try single object
120- var single api. ResourceProviderResource
191+ var single resourceInput
121192 if err := json .Unmarshal (data , & single ); err == nil {
122- return []api. ResourceProviderResource {single }, nil
193+ return []resourceInput {single }, nil
123194 }
124195
125196 // Show a snippet of the input for debugging
@@ -130,9 +201,24 @@ func parseResources(data []byte) ([]api.ResourceProviderResource, error) {
130201 return nil , fmt .Errorf ("invalid JSON input: %s" , snippet )
131202}
132203
204+ func toAPIResources (resources []resourceInput ) []api.ResourceProviderResource {
205+ out := make ([]api.ResourceProviderResource , 0 , len (resources ))
206+ for _ , r := range resources {
207+ out = append (out , api.ResourceProviderResource {
208+ Name : r .Name ,
209+ Identifier : r .Identifier ,
210+ Version : r .Version ,
211+ Kind : r .Kind ,
212+ Config : r .Config ,
213+ Metadata : r .Metadata ,
214+ })
215+ }
216+ return out
217+ }
218+
133219// validateResources checks that each resource has the required fields:
134220// Name, Identifier, Version, Kind.
135- func validateResources (resources []api. ResourceProviderResource ) error {
221+ func validateResources (resources []resourceInput ) error {
136222 var errs []string
137223 for i , r := range resources {
138224 var missing []string
@@ -157,3 +243,56 @@ func validateResources(resources []api.ResourceProviderResource) error {
157243 }
158244 return nil
159245}
246+
247+ func syncResourceVariables (
248+ ctx context.Context ,
249+ client * api.ClientWithResponses ,
250+ workspaceID string ,
251+ resources []resourceInput ,
252+ ) error {
253+ for _ , resource := range resources {
254+ if ! resource .hasVariables {
255+ continue
256+ }
257+
258+ vars := resource .Variables
259+ if vars == nil {
260+ vars = map [string ]any {}
261+ }
262+
263+ err := retry .Do (
264+ func () error {
265+ varsResp , err := client .RequestResourceVariablesUpdateWithResponse (
266+ ctx ,
267+ workspaceID ,
268+ resource .Identifier ,
269+ api .RequestResourceVariablesUpdateJSONRequestBody (vars ),
270+ )
271+ if err != nil {
272+ return retry .Unrecoverable (fmt .Errorf ("failed to update resource variables for '%s': %w" , resource .Identifier , err ))
273+ }
274+ if varsResp == nil {
275+ return retry .Unrecoverable (fmt .Errorf ("failed to update resource variables for '%s': empty response" , resource .Identifier ))
276+ }
277+ if varsResp .StatusCode () == 404 {
278+ return fmt .Errorf ("resource '%s' not found yet, retrying" , resource .Identifier )
279+ }
280+ if varsResp .StatusCode () != 204 {
281+ return retry .Unrecoverable (
282+ fmt .Errorf ("failed to update resource variables for '%s': %s" , resource .Identifier , string (varsResp .Body )),
283+ )
284+ }
285+ return nil
286+ },
287+ retry .Attempts (10 ),
288+ retry .Delay (100 * time .Millisecond ),
289+ retry .MaxDelay (15 * time .Second ),
290+ retry .DelayType (retry .BackOffDelay ),
291+ )
292+ if err != nil {
293+ return err
294+ }
295+ }
296+
297+ return nil
298+ }
0 commit comments