@@ -3,7 +3,7 @@ package create
33import (
44 "context"
55 "fmt"
6- "net/url "
6+ "strings "
77
88 "github.com/spf13/cobra"
99 "github.com/stackitcloud/stackit-cli/internal/cmd/params"
@@ -21,34 +21,101 @@ import (
2121)
2222
2323const (
24- regionsFlag = "regions"
25- originURLFlag = "origin-url"
24+ flagRegion = "regions"
25+ flagHTTP = "http"
26+ flagHTTPOriginURL = "http-origin-url"
27+ flagHTTPGeofencing = "http-geofencing"
28+ flagHTTPOriginRequestHeaders = "http-origin-request-header"
29+ flagBucket = "bucket"
30+ flagBucketURL = "bucket-url"
31+ flagBucketCredentialsAccessKeyID = "bucket-credentials-access-key-id"
32+ flagBucketRegion = "bucket-region"
33+ flagBlockedCountries = "blocked-countries"
34+ flagBlockedIPs = "blocked-ips"
35+ flagDefaultCacheDuration = "default-cache-duration"
36+ flagLoki = "loki"
37+ flagLokiUsername = "loki-username"
38+ flagLokiPushURL = "loki-push-url"
39+ flagMonthlyLimitBytes = "monthly-limit-bytes"
40+ flagOptimizer = "optimizer"
2641)
2742
43+ type httpInputModel struct {
44+ OriginURL string
45+ Geofencing * map [string ][]string
46+ OriginRequestHeaders * map [string ]string
47+ }
48+
49+ type bucketInputModel struct {
50+ URL string
51+ AccessKeyID string
52+ Password string
53+ Region string
54+ }
55+
56+ type lokiInputModel struct {
57+ Username string
58+ Password string
59+ PushURL string
60+ }
61+
2862type inputModel struct {
2963 * globalflags.GlobalFlagModel
30- Regions []cdn.Region
31- OriginURL string
64+ Regions []cdn.Region
65+ HTTP * httpInputModel
66+ Bucket * bucketInputModel
67+ BlockedCountries []string
68+ BlockedIPs []string
69+ DefaultCacheDuration string
70+ MonthlyLimitBytes * int64
71+ Loki * lokiInputModel
72+ Optimizer bool
3273}
3374
3475func NewCmd (params * params.CmdParams ) * cobra.Command {
3576 cmd := & cobra.Command {
36- Use : "create" ,
37- Short : "Create a CDN distribution" ,
38- Long : "Create a CDN distribution for a given originUrl in multiple regions." ,
39- Args : args .NoArgs ,
77+ Use : "create" ,
78+ Short : "Create a CDN distribution" ,
79+ Long : "Create a CDN distribution for a given originUrl in multiple regions." ,
80+ Args : args .NoArgs ,
4081 Example : examples .Build (
41- examples .NewExample (
42- `Create a distribution for regions EU and AF` ,
43- `$ stackit beta cdn distribution create --regions=EU,AF --origin-url=https://example.com` ,
44- ),
82+ //TODO
4583 ),
84+ PreRun : func (cmd * cobra.Command , args []string ) {
85+ // either flagHTTP or flagBucket must be set, depending on which we mark other flags as required
86+ if flags .FlagToBoolValue (params .Printer , cmd , flagHTTP ) {
87+ err := cmd .MarkFlagRequired (flagHTTPOriginURL )
88+ cobra .CheckErr (err )
89+ } else {
90+ err := flags .MarkFlagsRequired (cmd , flagBucketURL , flagBucketCredentialsAccessKeyID , flagBucketRegion )
91+ cobra .CheckErr (err )
92+ }
93+ // if user uses loki, mark related flags as required
94+ if flags .FlagToBoolValue (params .Printer , cmd , flagLoki ) {
95+ err := flags .MarkFlagsRequired (cmd , flagLokiUsername , flagLokiPushURL )
96+ cobra .CheckErr (err )
97+ }
98+ },
4699 RunE : func (cmd * cobra.Command , args []string ) error {
47100 ctx := context .Background ()
48101 model , err := parseInput (params .Printer , cmd , args )
49102 if err != nil {
50103 return err
51104 }
105+ if model .Bucket != nil {
106+ pw , err := params .Printer .PromptForPassword ("enter your secret access key for the object storage bucket: " )
107+ if err != nil {
108+ return fmt .Errorf ("reading secret access key: %w" , err )
109+ }
110+ model .Bucket .Password = pw
111+ }
112+ if model .Loki != nil {
113+ pw , err := params .Printer .PromptForPassword ("enter your password for the loki log sink: " )
114+ if err != nil {
115+ return fmt .Errorf ("reading loki password: %w" , err )
116+ }
117+ model .Loki .Password = pw
118+ }
52119
53120 apiClient , err := client .ConfigureClient (params .Printer , params .CliVersion )
54121 if err != nil {
@@ -84,9 +151,26 @@ func NewCmd(params *params.CmdParams) *cobra.Command {
84151}
85152
86153func configureFlags (cmd * cobra.Command ) {
87- cmd .Flags ().Var (flags .EnumSliceFlag (false , []string {}, sdkUtils .EnumSliceToStringSlice (cdn .AllowedRegionEnumValues )... ), regionsFlag , fmt .Sprintf ("Regions in which content should be cached, multiple of: %q" , cdn .AllowedRegionEnumValues ))
88- cmd .Flags ().String (originURLFlag , "" , "The origin of the content that should be made available through the CDN. Note that the path and query parameters are ignored. Ports are allowed. If no protocol is provided, `https` is assumed. So `www.example.com:1234/somePath?q=123` is normalized to `https://www.example.com:1234`" )
89- err := flags .MarkFlagsRequired (cmd , regionsFlag , originURLFlag )
154+ cmd .Flags ().Var (flags .EnumSliceFlag (false , []string {}, sdkUtils .EnumSliceToStringSlice (cdn .AllowedRegionEnumValues )... ), flagRegion , fmt .Sprintf ("Regions in which content should be cached, multiple of: %q" , cdn .AllowedRegionEnumValues ))
155+ cmd .Flags ().Bool (flagHTTP , false , "Use HTTP backend" )
156+ cmd .Flags ().String (flagHTTPOriginURL , "" , "Origin URL for HTTP backend" )
157+ cmd .Flags ().StringSlice (flagHTTPOriginRequestHeaders , []string {}, "Origin request headers for HTTP backend in the format 'HeaderName: HeaderValue', repeatable. WARNING: do not store sensitive values in the headers!" )
158+ cmd .Flags ().StringArray (flagHTTPGeofencing , []string {}, "Geofencing rules for HTTP backend in the format 'https://example.com US,DE'. URL and countries have to be quoted. Repeatable." )
159+ cmd .Flags ().Bool (flagBucket , false , "Use Object Storage backend" )
160+ cmd .Flags ().String (flagBucketURL , "" , "Bucket URL for Object Storage backend" )
161+ cmd .Flags ().String (flagBucketCredentialsAccessKeyID , "" , "Access Key ID for Object Storage backend" )
162+ cmd .Flags ().String (flagBucketRegion , "" , "Region for Object Storage backend" )
163+ cmd .Flags ().StringSlice (flagBlockedCountries , []string {}, "Comma-separated list of ISO 3166-1 alpha-2 country codes to block (e.g., 'US,DE,FR')" )
164+ cmd .Flags ().StringSlice (flagBlockedIPs , []string {}, "Comma-separated list of IPv4 addresses to block (e.g., '10.0.0.8,127.0.0.1')" )
165+ cmd .Flags ().String (flagDefaultCacheDuration , "" , "ISO8601 duration string for default cache duration (e.g., 'PT1H30M' for 1 hour and 30 minutes)" )
166+ cmd .Flags ().Bool (flagLoki , false , "Enable Loki log sink for the CDN distribution" )
167+ cmd .Flags ().String (flagLokiUsername , "" , "Username for log sink" )
168+ cmd .Flags ().String (flagLokiPushURL , "" , "Push URL for log sink" )
169+ cmd .Flags ().Int64 (flagMonthlyLimitBytes , 0 , "Monthly limit in bytes for the CDN distribution" )
170+ cmd .Flags ().Bool (flagOptimizer , false , "Enable optimizer for the CDN distribution (paid feature)." )
171+ cmd .MarkFlagsMutuallyExclusive (flagHTTP , flagBucket )
172+ cmd .MarkFlagsOneRequired (flagHTTP , flagBucket )
173+ err := flags .MarkFlagsRequired (cmd , flagRegion )
90174 cobra .CheckErr (err )
91175}
92176
@@ -96,33 +180,171 @@ func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel,
96180 return nil , & errors.ProjectIdError {}
97181 }
98182
99- regionStrings := flags .FlagToStringSliceValue (p , cmd , regionsFlag )
183+ regionStrings := flags .FlagToStringSliceValue (p , cmd , flagRegion )
100184 regions := make ([]cdn.Region , 0 , len (regionStrings ))
101185 for _ , regionStr := range regionStrings {
102186 regions = append (regions , cdn .Region (regionStr ))
103187 }
104188
105- originUrlString := flags .FlagToStringValue (p , cmd , originURLFlag )
106- _ , err := url .Parse (originUrlString )
107- if err != nil {
108- return nil , fmt .Errorf ("invalid originUrl: '%s' (%w)" , originUrlString , err )
189+ var http * httpInputModel
190+ if flags .FlagToBoolValue (p , cmd , flagHTTP ) {
191+ originURL := flags .FlagToStringValue (p , cmd , flagHTTPOriginURL )
192+
193+ var geofencing * map [string ][]string
194+ geofencingInput := flags .FlagToStringArrayValue (p , cmd , flagHTTPGeofencing )
195+ if geofencingInput != nil {
196+ geofencing = parseGeofencing (p , geofencingInput )
197+ }
198+
199+ var originRequestHeaders * map [string ]string
200+ originRequestHeadersInput := flags .FlagToStringSliceValue (p , cmd , flagHTTPOriginRequestHeaders )
201+ if originRequestHeadersInput != nil {
202+ originRequestHeaders = parseOriginRequestHeaders (p , originRequestHeadersInput )
203+ }
204+
205+ http = & httpInputModel {
206+ OriginURL : originURL ,
207+ Geofencing : geofencing ,
208+ OriginRequestHeaders : originRequestHeaders ,
209+ }
210+ }
211+
212+ var bucket * bucketInputModel
213+ if flags .FlagToBoolValue (p , cmd , flagBucket ) {
214+ bucketURL := flags .FlagToStringValue (p , cmd , flagBucketURL )
215+ accessKeyID := flags .FlagToStringValue (p , cmd , flagBucketCredentialsAccessKeyID )
216+ region := flags .FlagToStringValue (p , cmd , flagBucketRegion )
217+
218+ bucket = & bucketInputModel {
219+ URL : bucketURL ,
220+ AccessKeyID : accessKeyID ,
221+ Password : "" ,
222+ Region : region ,
223+ }
224+ }
225+
226+ blockedCountries := flags .FlagToStringSliceValue (p , cmd , flagBlockedCountries )
227+ blockedIPs := flags .FlagToStringSliceValue (p , cmd , flagBlockedIPs )
228+ cacheDuration := flags .FlagToStringValue (p , cmd , flagDefaultCacheDuration )
229+ monthlyLimit := flags .FlagToInt64Pointer (p , cmd , flagMonthlyLimitBytes )
230+
231+ var loki * lokiInputModel
232+ if flags .FlagToBoolValue (p , cmd , flagLoki ) {
233+ loki = & lokiInputModel {
234+ Username : flags .FlagToStringValue (p , cmd , flagLokiUsername ),
235+ PushURL : flags .FlagToStringValue (p , cmd , flagLokiPushURL ),
236+ Password : "" ,
237+ }
109238 }
110239
240+ optimizer := flags .FlagToBoolValue (p , cmd , flagOptimizer )
241+
111242 model := inputModel {
112- GlobalFlagModel : globalFlags ,
113- Regions : regions ,
114- OriginURL : originUrlString ,
243+ GlobalFlagModel : globalFlags ,
244+ Regions : regions ,
245+ HTTP : http ,
246+ Bucket : bucket ,
247+ BlockedCountries : blockedCountries ,
248+ BlockedIPs : blockedIPs ,
249+ DefaultCacheDuration : cacheDuration ,
250+ MonthlyLimitBytes : monthlyLimit ,
251+ Loki : loki ,
252+ Optimizer : optimizer ,
115253 }
116254
117255 return & model , nil
118256}
119257
258+ func parseGeofencing (p * print.Printer , geofencingInput []string ) * map [string ][]string {
259+ geofencing := make (map [string ][]string )
260+ for _ , in := range geofencingInput {
261+ firstSpace := strings .IndexRune (in , ' ' )
262+ if firstSpace == - 1 {
263+ p .Debug (print .ErrorLevel , "invalid geofencing entry (no space found): %q" , in )
264+ continue
265+ }
266+ urlPart := in [:firstSpace ]
267+ countriesPart := in [firstSpace + 1 :]
268+ geofencing [urlPart ] = nil
269+ countries := strings .Split (countriesPart , "," )
270+ for _ , country := range countries {
271+ country = strings .TrimSpace (country )
272+ geofencing [urlPart ] = append (geofencing [urlPart ], country )
273+ }
274+ }
275+ return & geofencing
276+ }
277+
278+ func parseOriginRequestHeaders (p * print.Printer , originRequestHeadersInput []string ) * map [string ]string {
279+ originRequestHeaders := make (map [string ]string )
280+ for _ , in := range originRequestHeadersInput {
281+ parts := strings .Split (in , ":" )
282+ if len (parts ) != 2 {
283+ p .Debug (print .ErrorLevel , "invalid origin request header entry (no colon found): %q" , in )
284+ continue
285+ }
286+ originRequestHeaders [strings .TrimSpace (parts [0 ])] = strings .TrimSpace (parts [1 ])
287+ }
288+ return & originRequestHeaders
289+ }
290+
120291func buildRequest (ctx context.Context , model * inputModel , apiClient * cdn.APIClient ) cdn.ApiCreateDistributionRequest {
121292 req := apiClient .CreateDistribution (ctx , model .ProjectId )
293+ var backend cdn.CreateDistributionPayloadGetBackendArgType
294+ if model .HTTP != nil {
295+ backend = cdn.CreateDistributionPayloadGetBackendArgType {
296+ HttpBackendCreate : & cdn.HttpBackendCreate {
297+ Geofencing : model .HTTP .Geofencing ,
298+ OriginRequestHeaders : model .HTTP .OriginRequestHeaders ,
299+ OriginUrl : & model .HTTP .OriginURL ,
300+ Type : utils .Ptr ("http" ),
301+ },
302+ }
303+ } else {
304+ backend = cdn.CreateDistributionPayloadGetBackendArgType {
305+ BucketBackendCreate : & cdn.BucketBackendCreate {
306+ BucketUrl : & model .Bucket .URL ,
307+ Credentials : cdn .NewBucketCredentials (
308+ model .Bucket .AccessKeyID ,
309+ model .Bucket .Password ,
310+ ),
311+ Region : & model .Bucket .Region ,
312+ Type : utils .Ptr ("bucket" ),
313+ },
314+ }
315+ }
316+
122317 payload := cdn .NewCreateDistributionPayload (
123- model . OriginURL ,
318+ backend ,
124319 model .Regions ,
125320 )
321+ if len (model .BlockedCountries ) > 0 {
322+ payload .BlockedCountries = & model .BlockedCountries
323+ }
324+ if len (model .BlockedIPs ) > 0 {
325+ payload .BlockedIps = & model .BlockedIPs
326+ }
327+ if model .DefaultCacheDuration != "" {
328+ payload .DefaultCacheDuration = utils .Ptr (model .DefaultCacheDuration )
329+ }
330+ if model .Loki != nil {
331+ payload .LogSink = & cdn.CreateDistributionPayloadGetLogSinkArgType {
332+ LokiLogSinkCreate : & cdn.LokiLogSinkCreate {
333+ Credentials : & cdn.LokiLogSinkCredentials {
334+ Password : & model .Loki .Password ,
335+ Username : & model .Loki .Username ,
336+ },
337+ PushUrl : & model .Loki .PushURL ,
338+ Type : utils .Ptr ("loki" ),
339+ },
340+ }
341+ }
342+ payload .MonthlyLimitBytes = model .MonthlyLimitBytes
343+ if model .Optimizer {
344+ payload .Optimizer = & cdn.CreateDistributionPayloadGetOptimizerArgType {
345+ Enabled : utils .Ptr (true ),
346+ }
347+ }
126348 return req .CreateDistributionPayload (* payload )
127349}
128350
0 commit comments