Skip to content

Commit 1235792

Browse files
committed
feat(cdn) implement cdn distribution create/delete/describe/update
1 parent 7296ee0 commit 1235792

File tree

14 files changed

+2115
-124
lines changed

14 files changed

+2115
-124
lines changed

go.mod

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ require (
1818
github.com/stackitcloud/stackit-sdk-go/core v0.20.0
1919
github.com/stackitcloud/stackit-sdk-go/services/alb v0.7.1
2020
github.com/stackitcloud/stackit-sdk-go/services/authorization v0.9.0
21-
github.com/stackitcloud/stackit-sdk-go/services/cdn v1.6.0
21+
github.com/stackitcloud/stackit-sdk-go/services/cdn v1.8.1
2222
github.com/stackitcloud/stackit-sdk-go/services/dns v0.17.1
2323
github.com/stackitcloud/stackit-sdk-go/services/git v0.9.1
2424
github.com/stackitcloud/stackit-sdk-go/services/iaas v1.2.0
@@ -36,6 +36,7 @@ require (
3636
github.com/stackitcloud/stackit-sdk-go/services/ske v1.4.0
3737
github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex v1.3.2
3838
github.com/zalando/go-keyring v0.2.6
39+
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0
3940
golang.org/x/mod v0.30.0
4041
golang.org/x/oauth2 v0.33.0
4142
golang.org/x/term v0.37.0
@@ -258,7 +259,7 @@ require (
258259
gopkg.in/yaml.v3 v3.0.1 // indirect
259260
k8s.io/api v0.34.2 // indirect
260261
k8s.io/klog/v2 v2.130.1 // indirect
261-
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect
262+
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397
262263
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
263264
sigs.k8s.io/yaml v1.6.0 // indirect
264265
)

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -567,8 +567,8 @@ github.com/stackitcloud/stackit-sdk-go/services/alb v0.7.1 h1:DaJkEN/6l+AJEQ3Dr+
567567
github.com/stackitcloud/stackit-sdk-go/services/alb v0.7.1/go.mod h1:SzA+UsSNv4D9IvNT7hwYPewgAvUgj5WXIU2tZ0XaMBI=
568568
github.com/stackitcloud/stackit-sdk-go/services/authorization v0.9.0 h1:7ZKd3b+E/R4TEVShLTXxx5FrsuDuJBOyuVOuKTMa4mo=
569569
github.com/stackitcloud/stackit-sdk-go/services/authorization v0.9.0/go.mod h1:/FoXa6hF77Gv8brrvLBCKa5ie1Xy9xn39yfHwaln9Tw=
570-
github.com/stackitcloud/stackit-sdk-go/services/cdn v1.6.0 h1:Q+qIdejeMsYMkbtVoI9BpGlKGdSVFRBhH/zj44SP8TM=
571-
github.com/stackitcloud/stackit-sdk-go/services/cdn v1.6.0/go.mod h1:YGadfhuy8yoseczTxF7vN4t9ES2WxGQr0Pug14ii7y4=
570+
github.com/stackitcloud/stackit-sdk-go/services/cdn v1.8.1 h1:CiOlfCsCDwHP0kas7qyhfp5XtL2kVmn9e4wjtc3LO10=
571+
github.com/stackitcloud/stackit-sdk-go/services/cdn v1.8.1/go.mod h1:PyZ6g9JsGZZyeISAF+5E7L1lAlMnmbl2YbPj5Teu8to=
572572
github.com/stackitcloud/stackit-sdk-go/services/dns v0.17.1 h1:CnhAMLql0MNmAeq4roQKN8OpSKX4FSgTU6Eu6detB4I=
573573
github.com/stackitcloud/stackit-sdk-go/services/dns v0.17.1/go.mod h1:7Bx85knfNSBxulPdJUFuBePXNee3cO+sOTYnUG6M+iQ=
574574
github.com/stackitcloud/stackit-sdk-go/services/git v0.9.1 h1:RgWfaWDY8ZGZp5gEBe/A1r7s5NCRuLiYuHhscH6Ej9U=

internal/cmd/beta/cdn/distribution/create/create.go

Lines changed: 247 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ package create
33
import (
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

2323
const (
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+
2862
type 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

3475
func 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

86153
func 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+
120291
func 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

Comments
 (0)