@@ -2,6 +2,8 @@ package blueprint
22
33import (
44 "errors"
5+ "fmt"
6+ "io"
57 "os"
68 "path/filepath"
79 "regexp"
@@ -14,8 +16,11 @@ import (
1416)
1517
1618type BlueprintCmdOpts struct {
17- Name string
18- Path string
19+ Name string
20+ Path string
21+ APIKey string
22+ Endpoint string
23+ OrgID string
1924}
2025
2126func BlueprintCmd () * cobra.Command {
@@ -26,78 +31,112 @@ func BlueprintCmd() *cobra.Command {
2631 Short : "Apply a blueprint" ,
2732 Long : "Apply a YAML blueprint to the Pangolin server" ,
2833 PreRunE : func (cmd * cobra.Command , args []string ) error {
29- if opts .Path == "" {
30- return errors .New ("--file is required" )
34+ // Integration API: any of the three flags implies all three are required (avoids silent session fallback).
35+ integration := opts .APIKey != "" || opts .Endpoint != "" || opts .OrgID != ""
36+ if integration && (opts .APIKey == "" || opts .Endpoint == "" || opts .OrgID == "" ) {
37+ return errors .New ("integration API mode requires --api-key, --endpoint, and --org together; omit all three to use your logged-in session and selected org" )
3138 }
32-
33- if _ , err := os .Stat (opts .Path ); err != nil {
34- return err
35- }
36-
37- // Strip file extension and use file basename path as name
38- if opts .Name == "" {
39- filename := filepath .Base (opts .Path )
40- if before , ok := strings .CutSuffix (filename , ".yaml" ); ok {
41- opts .Name = before
42- } else if before , ok := strings .CutSuffix (filename , ".yml" ); ok {
43- opts .Name = before
44- } else {
45- opts .Name = filename
46- }
47- }
48-
49- if len (opts .Name ) < 1 || len (opts .Name ) > 255 {
50- return errors .New ("name must be between 1-255 characters" )
51- }
52-
5339 return nil
5440 },
55- Run : func (cmd * cobra.Command , args []string ) {
41+ RunE : func (cmd * cobra.Command , args []string ) error {
5642 if err := applyBlueprintMain (cmd , opts ); err != nil {
57- os . Exit ( 1 )
43+ return err
5844 }
45+ logger .Info ("Successfully applied blueprint!" )
46+ return nil
5947 },
6048 }
6149
62- cmd .Flags ().StringVarP (& opts .Path , "file" , "f" , "" , "Path to blueprint file (required)" )
63- cmd .Flags ().StringVarP (& opts .Name , "name" , "n" , "" , "Name of blueprint (default: filename, without extension)" )
50+ cmd .Flags ().StringVarP (& opts .Path , "file" , "f" , "" , "Blueprint YAML file path (use '-' for stdin)" )
51+ cmd .Flags ().StringVarP (& opts .Name , "name" , "n" , "" , "Blueprint name (default: filename without extension)" )
52+ cmd .Flags ().StringVar (& opts .APIKey , "api-key" , "" , "Integration API key (id.secret)" )
53+ cmd .Flags ().StringVar (& opts .Endpoint , "endpoint" , "" , "Integration API host URL" )
54+ cmd .Flags ().StringVar (& opts .OrgID , "org" , "" , "Organization ID" )
6455 cmd .MarkFlagRequired ("file" )
6556
6657 return cmd
6758}
6859
6960func applyBlueprintMain (cmd * cobra.Command , opts BlueprintCmdOpts ) error {
70- api := api .FromContext (cmd .Context ())
71- accountStore := config .AccountStoreFromContext (cmd .Context ())
72-
73- account , err := accountStore .ActiveAccount ()
74- if err != nil {
75- logger .Error ("Error: %v" , err )
76- return err
61+ if opts .Path == "-" && strings .TrimSpace (opts .Name ) == "" {
62+ return errors .New ("name is required when using --file -" )
7763 }
7864
79- if account .OrgID == "" {
80- logger .Error ("Error: no organization selected. Run 'pangolin select org' first." )
81- return errors .New ("no organization selected" )
65+ name := opts .Name
66+ if name == "" {
67+ filename := filepath .Base (opts .Path )
68+ switch ext := strings .ToLower (filepath .Ext (filename )); ext {
69+ case ".yaml" , ".yml" :
70+ name = strings .TrimSuffix (filename , ext )
71+ default :
72+ name = filename
73+ }
74+ }
75+ if len (name ) < 1 || len (name ) > 255 {
76+ return errors .New ("name must be between 1-255 characters" )
8277 }
8378
84- blueprintContents , err := os .ReadFile (opts .Path )
79+ apiClient := api .FromContext (cmd .Context ())
80+ accountStore := config .AccountStoreFromContext (cmd .Context ())
81+
82+ blueprintContents , err := readBlueprint (opts .Path )
8583 if err != nil {
86- logger .Error ("Error: failed to read blueprint file: %v" , err )
8784 return err
8885 }
8986
9087 blueprintContents = interpolateBlueprint (blueprintContents )
9188
92- _ , err = api .ApplyBlueprint (account .OrgID , opts .Name , string (blueprintContents ))
89+ client := apiClient
90+ orgID := opts .OrgID
91+
92+ if opts .APIKey != "" {
93+ client , err = apiClient .WithIntegrationAPIKey (opts .Endpoint , opts .APIKey )
94+ if err != nil {
95+ return fmt .Errorf ("failed to initialize api key client: %w" , err )
96+ }
97+ } else {
98+ account , errAcc := accountStore .ActiveAccount ()
99+ if errAcc != nil {
100+ return errAcc
101+ }
102+ if account .OrgID == "" {
103+ return errors .New ("no organization selected" )
104+ }
105+ orgID = account .OrgID
106+ }
107+
108+ _ , err = client .ApplyBlueprint (orgID , name , string (blueprintContents ))
93109 if err != nil {
94- logger .Error ("Error: failed to apply blueprint: %v" , err )
95- return err
110+ return fmt .Errorf ("failed to apply blueprint: %w" , err )
96111 }
112+ return nil
113+ }
97114
98- logger .Info ("Successfully applied blueprint!" )
115+ func readBlueprint (path string ) ([]byte , error ) {
116+ if path == "-" {
117+ fileInfo , err := os .Stdin .Stat ()
118+ if err != nil {
119+ return nil , fmt .Errorf ("failed to inspect stdin: %w" , err )
120+ }
121+ if (fileInfo .Mode () & os .ModeCharDevice ) == os .ModeCharDevice {
122+ return nil , errors .New ("the option --file - is intended to work with pipes" )
123+ }
99124
100- return nil
125+ contents , err := io .ReadAll (os .Stdin )
126+ if err != nil {
127+ return nil , fmt .Errorf ("failed to read blueprint from stdin: %w" , err )
128+ }
129+ if len (contents ) == 0 {
130+ return nil , errors .New ("blueprint input is empty" )
131+ }
132+ return contents , nil
133+ }
134+
135+ contents , err := os .ReadFile (path )
136+ if err != nil {
137+ return nil , fmt .Errorf ("failed to read blueprint file: %w" , err )
138+ }
139+ return contents , nil
101140}
102141
103142// interpolateBlueprint finds all {{...}} tokens in the raw blueprint bytes and
0 commit comments