Skip to content

Commit 58f932b

Browse files
feat(service-resource): Add Service Resource commands
https://developer.fastly.com/reference/api/services/resource/ This adds a `service-resource` sub-command which has associated create, delete, list, and update commands. Some of the names in the API may be confusing. I've done my best to add clarification whenever possible. Specifically: - A resource, as named by the API, is truly a _link_ between a service and a resource (e.g. an Object Store), not the resource itself. I've tried to use "resource **link**" wherever appropriate, as also used by the API docs: > A resource represents a link between a resource type and a service version. - In some places, the API mixes `id` and `resource_id` which both refer to the ID of the resource _link_, and not the resource itself. I've changed this so that `id` always refers the resource link, and `resource_id` to the linked to object. See this code comment: https://github.com/fastly/go-fastly/blob/ef4694a59779f202d8ea630b41333eb2b76969e8/fastly/resource.go#L125-L129 - It's possible to provide a resource link name. If set, this is an alias for the resource. Otherwise, whatever name was used when creating the resource itself will be the name to reference from the service for the resource. I've added to the description that this is an _alias_. See this code comment: https://github.com/fastly/go-fastly/blob/ef4694a59779f202d8ea630b41333eb2b76969e8/fastly/resource.go#L86-L88 I'd like to look into modifying `go-fastly` to remove the need for the `jsonResource` type, and also into updating the API to be more consistent with `id` vs `resource_id`.
1 parent 894a1f3 commit 58f932b

File tree

14 files changed

+1353
-0
lines changed

14 files changed

+1353
-0
lines changed

pkg/api/interface.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,10 @@ type Interface interface {
347347
ListSecrets(i *fastly.ListSecretsInput) (*fastly.Secrets, error)
348348

349349
CreateResource(i *fastly.CreateResourceInput) (*fastly.Resource, error)
350+
DeleteResource(i *fastly.DeleteResourceInput) error
351+
GetResource(i *fastly.GetResourceInput) (*fastly.Resource, error)
352+
ListResources(i *fastly.ListResourcesInput) ([]*fastly.Resource, error)
353+
UpdateResource(i *fastly.UpdateResourceInput) (*fastly.Resource, error)
350354
}
351355

352356
// RealtimeStatsInterface is the subset of go-fastly's realtime stats API used here.

pkg/app/commands.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ import (
5050
"github.com/fastly/cli/pkg/commands/secretstoreentry"
5151
"github.com/fastly/cli/pkg/commands/service"
5252
"github.com/fastly/cli/pkg/commands/serviceauth"
53+
"github.com/fastly/cli/pkg/commands/serviceresource"
5354
"github.com/fastly/cli/pkg/commands/serviceversion"
5455
"github.com/fastly/cli/pkg/commands/shellcomplete"
5556
"github.com/fastly/cli/pkg/commands/stats"
@@ -341,6 +342,12 @@ func defineCommands(
341342
serviceauthDescribe := serviceauth.NewDescribeCommand(serviceauthCmdRoot.CmdClause, g, m)
342343
serviceauthList := serviceauth.NewListCommand(serviceauthCmdRoot.CmdClause, g)
343344
serviceauthUpdate := serviceauth.NewUpdateCommand(serviceauthCmdRoot.CmdClause, g, m)
345+
serviceresourceCmdRoot := serviceresource.NewRootCommand(app, g)
346+
serviceresourceCreate := serviceresource.NewCreateCommand(serviceresourceCmdRoot.CmdClause, g, m)
347+
serviceresourceDelete := serviceresource.NewDeleteCommand(serviceresourceCmdRoot.CmdClause, g, m)
348+
serviceresourceDescribe := serviceresource.NewDescribeCommand(serviceresourceCmdRoot.CmdClause, g, m)
349+
serviceresourceList := serviceresource.NewListCommand(serviceresourceCmdRoot.CmdClause, g, m)
350+
serviceresourceUpdate := serviceresource.NewUpdateCommand(serviceresourceCmdRoot.CmdClause, g, m)
344351
serviceVersionCmdRoot := serviceversion.NewRootCommand(app, g)
345352
serviceVersionActivate := serviceversion.NewActivateCommand(serviceVersionCmdRoot.CmdClause, g, m)
346353
serviceVersionClone := serviceversion.NewCloneCommand(serviceVersionCmdRoot.CmdClause, g, m)
@@ -669,6 +676,12 @@ func defineCommands(
669676
serviceauthDescribe,
670677
serviceauthList,
671678
serviceauthUpdate,
679+
serviceresourceCmdRoot,
680+
serviceresourceCreate,
681+
serviceresourceDelete,
682+
serviceresourceDescribe,
683+
serviceresourceList,
684+
serviceresourceUpdate,
672685
serviceVersionActivate,
673686
serviceVersionClone,
674687
serviceVersionCmdRoot,

pkg/app/run_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ secret-store
7979
secret-store-entry
8080
service
8181
service-auth
82+
service-resource
8283
service-version
8384
stats
8485
tls-config
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
package serviceresource
2+
3+
import (
4+
"io"
5+
6+
"github.com/fastly/cli/pkg/cmd"
7+
fsterr "github.com/fastly/cli/pkg/errors"
8+
"github.com/fastly/cli/pkg/global"
9+
"github.com/fastly/cli/pkg/manifest"
10+
"github.com/fastly/cli/pkg/text"
11+
"github.com/fastly/go-fastly/v7/fastly"
12+
)
13+
14+
// CreateCommand calls the Fastly API to create a resource link.
15+
type CreateCommand struct {
16+
cmd.Base
17+
jsonOutput
18+
19+
autoClone cmd.OptionalAutoClone
20+
input fastly.CreateResourceInput
21+
manifest manifest.Data
22+
serviceVersion cmd.OptionalServiceVersion
23+
}
24+
25+
// NewCreateCommand returns a usable command registered under the parent.
26+
func NewCreateCommand(parent cmd.Registerer, globals *global.Data, data manifest.Data) *CreateCommand {
27+
c := CreateCommand{
28+
Base: cmd.Base{
29+
Globals: globals,
30+
},
31+
manifest: data,
32+
input: fastly.CreateResourceInput{
33+
// Kingpin requires the following to be initialized.
34+
ResourceID: new(string),
35+
Name: new(string),
36+
},
37+
}
38+
c.CmdClause = parent.Command("create", "Create a Fastly service resource link").Alias("link")
39+
40+
// Required.
41+
c.RegisterFlag(cmd.StringFlagOpts{
42+
Name: "resource-id",
43+
Short: 'r',
44+
Description: "Resource ID",
45+
Dst: c.input.ResourceID,
46+
Required: true,
47+
})
48+
c.RegisterFlag(cmd.StringFlagOpts{
49+
Name: cmd.FlagServiceIDName,
50+
Short: 's',
51+
Description: cmd.FlagServiceIDDesc,
52+
Dst: &c.manifest.Flag.ServiceID,
53+
Required: true,
54+
})
55+
c.RegisterFlag(cmd.StringFlagOpts{
56+
Name: cmd.FlagVersionName,
57+
Description: cmd.FlagVersionDesc,
58+
Dst: &c.serviceVersion.Value,
59+
Required: true,
60+
})
61+
62+
// Optional.
63+
c.RegisterAutoCloneFlag(cmd.AutoCloneFlagOpts{
64+
Action: c.autoClone.Set,
65+
Dst: &c.autoClone.Value,
66+
})
67+
c.RegisterFlagBool(c.jsonFlag()) // --json
68+
c.RegisterFlag(cmd.StringFlagOpts{
69+
Name: "name",
70+
Short: 'n',
71+
Description: "Resource alias. Defaults to name of resource",
72+
Dst: c.input.Name,
73+
})
74+
75+
return &c
76+
}
77+
78+
// Exec invokes the application logic for the command.
79+
func (c *CreateCommand) Exec(_ io.Reader, out io.Writer) error {
80+
if c.Globals.Verbose() && c.jsonOutput.enabled {
81+
return fsterr.ErrInvalidVerboseJSONCombo
82+
}
83+
84+
serviceID, serviceVersion, err := cmd.ServiceDetails(cmd.ServiceDetailsOpts{
85+
AutoCloneFlag: c.autoClone,
86+
APIClient: c.Globals.APIClient,
87+
Manifest: c.manifest,
88+
Out: out,
89+
ServiceNameFlag: cmd.OptionalServiceNameID{}, // ServiceID flag is required, no need to lookup service by name.
90+
ServiceVersionFlag: c.serviceVersion,
91+
VerboseMode: c.Globals.Flags.Verbose,
92+
})
93+
if err != nil {
94+
c.Globals.ErrLog.AddWithContext(err, map[string]any{
95+
"Service ID": c.manifest.Flag.ServiceID,
96+
"Service Version": fsterr.ServiceVersion(serviceVersion),
97+
})
98+
return err
99+
}
100+
101+
c.input.ServiceID = serviceID
102+
c.input.ServiceVersion = serviceVersion.Number
103+
104+
resource, err := c.Globals.APIClient.CreateResource(&c.input)
105+
if err != nil {
106+
c.Globals.ErrLog.AddWithContext(err, map[string]any{
107+
"ID": c.input.ResourceID,
108+
"Service ID": c.input.ServiceID,
109+
"Service Version": c.input.ServiceVersion,
110+
})
111+
return err
112+
}
113+
114+
if ok, err := c.WriteJSON(out, resource); ok {
115+
return err
116+
}
117+
118+
text.Success(out, "Created service resource link %s on service %s version %s", resource.ID, resource.ServiceID, resource.ServiceVersion)
119+
return nil
120+
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
package serviceresource
2+
3+
import (
4+
"io"
5+
6+
"github.com/fastly/cli/pkg/cmd"
7+
fsterr "github.com/fastly/cli/pkg/errors"
8+
"github.com/fastly/cli/pkg/global"
9+
"github.com/fastly/cli/pkg/manifest"
10+
"github.com/fastly/cli/pkg/text"
11+
"github.com/fastly/go-fastly/v7/fastly"
12+
)
13+
14+
// DeleteCommand calls the Fastly API to delete service resource links.
15+
type DeleteCommand struct {
16+
cmd.Base
17+
jsonOutput
18+
19+
autoClone cmd.OptionalAutoClone
20+
input fastly.DeleteResourceInput
21+
manifest manifest.Data
22+
serviceVersion cmd.OptionalServiceVersion
23+
}
24+
25+
// NewDeleteCommand returns a usable command registered under the parent.
26+
func NewDeleteCommand(parent cmd.Registerer, globals *global.Data, data manifest.Data) *DeleteCommand {
27+
c := DeleteCommand{
28+
Base: cmd.Base{
29+
Globals: globals,
30+
},
31+
manifest: data,
32+
}
33+
c.CmdClause = parent.Command("delete", "Delete a resource link for a Fastly service version").Alias("remove")
34+
35+
// Required.
36+
c.RegisterFlag(cmd.StringFlagOpts{
37+
Name: "id",
38+
Description: "ID of resource link",
39+
Dst: &c.input.ResourceID,
40+
Required: true,
41+
})
42+
c.RegisterFlag(cmd.StringFlagOpts{
43+
Name: cmd.FlagServiceIDName,
44+
Short: 's',
45+
Description: cmd.FlagServiceIDDesc,
46+
Dst: &c.manifest.Flag.ServiceID,
47+
Required: true,
48+
})
49+
c.RegisterFlag(cmd.StringFlagOpts{
50+
Name: cmd.FlagVersionName,
51+
Description: cmd.FlagVersionDesc,
52+
Dst: &c.serviceVersion.Value,
53+
Required: true,
54+
})
55+
56+
// Optional.
57+
c.RegisterAutoCloneFlag(cmd.AutoCloneFlagOpts{
58+
Action: c.autoClone.Set,
59+
Dst: &c.autoClone.Value,
60+
})
61+
c.RegisterFlagBool(c.jsonFlag()) // --json
62+
63+
return &c
64+
}
65+
66+
// Exec invokes the application logic for the command.
67+
func (c *DeleteCommand) Exec(_ io.Reader, out io.Writer) error {
68+
if c.Globals.Verbose() && c.jsonOutput.enabled {
69+
return fsterr.ErrInvalidVerboseJSONCombo
70+
}
71+
72+
serviceID, serviceVersion, err := cmd.ServiceDetails(cmd.ServiceDetailsOpts{
73+
AutoCloneFlag: c.autoClone,
74+
APIClient: c.Globals.APIClient,
75+
Manifest: c.manifest,
76+
Out: out,
77+
ServiceNameFlag: cmd.OptionalServiceNameID{}, // ServiceID flag is required, no need to lookup service by name.
78+
ServiceVersionFlag: c.serviceVersion,
79+
VerboseMode: c.Globals.Flags.Verbose,
80+
})
81+
if err != nil {
82+
c.Globals.ErrLog.AddWithContext(err, map[string]any{
83+
"Service ID": c.manifest.Flag.ServiceID,
84+
"Service Version": fsterr.ServiceVersion(serviceVersion),
85+
})
86+
return err
87+
}
88+
89+
c.input.ServiceID = serviceID
90+
c.input.ServiceVersion = serviceVersion.Number
91+
92+
err = c.Globals.APIClient.DeleteResource(&c.input)
93+
if err != nil {
94+
c.Globals.ErrLog.AddWithContext(err, map[string]any{
95+
"ID": c.input.ResourceID,
96+
"Service ID": c.input.ServiceID,
97+
"Service Version": c.input.ServiceVersion,
98+
})
99+
return err
100+
}
101+
102+
if c.jsonOutput.enabled {
103+
o := struct {
104+
ResourceID string `json:"id"`
105+
ServiceID string `json:"service_id"`
106+
ServiceVersion int `json:"service_version"`
107+
Deleted bool `json:"deleted"`
108+
}{
109+
c.input.ResourceID,
110+
c.input.ServiceID,
111+
c.input.ServiceVersion,
112+
true,
113+
}
114+
_, err := c.WriteJSON(out, o)
115+
return err
116+
}
117+
118+
text.Success(out, "Deleted service resource link %s from service %s version %d", c.input.ResourceID, c.input.ServiceID, c.input.ServiceVersion)
119+
return nil
120+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package serviceresource
2+
3+
import (
4+
"io"
5+
6+
"github.com/fastly/cli/pkg/cmd"
7+
fsterr "github.com/fastly/cli/pkg/errors"
8+
"github.com/fastly/cli/pkg/global"
9+
"github.com/fastly/cli/pkg/manifest"
10+
"github.com/fastly/cli/pkg/text"
11+
"github.com/fastly/go-fastly/v7/fastly"
12+
)
13+
14+
// DescribeCommand calls the Fastly API to describe a service resource link.
15+
type DescribeCommand struct {
16+
cmd.Base
17+
jsonOutput
18+
19+
input fastly.GetResourceInput
20+
manifest manifest.Data
21+
serviceVersion cmd.OptionalServiceVersion
22+
}
23+
24+
// NewDescribeCommand returns a usable command registered under the parent.
25+
func NewDescribeCommand(parent cmd.Registerer, globals *global.Data, data manifest.Data) *DescribeCommand {
26+
c := DescribeCommand{
27+
Base: cmd.Base{
28+
Globals: globals,
29+
},
30+
manifest: data,
31+
}
32+
c.CmdClause = parent.Command("describe", "Show detailed information about a Fastly service resource link").Alias("get")
33+
34+
// Required.
35+
c.RegisterFlag(cmd.StringFlagOpts{
36+
Name: "id",
37+
Description: "ID of resource link",
38+
Dst: &c.input.ResourceID,
39+
Required: true,
40+
})
41+
c.RegisterFlag(cmd.StringFlagOpts{
42+
Name: cmd.FlagServiceIDName,
43+
Short: 's',
44+
Description: cmd.FlagServiceIDDesc,
45+
Dst: &c.manifest.Flag.ServiceID,
46+
Required: true,
47+
})
48+
c.RegisterFlag(cmd.StringFlagOpts{
49+
Name: cmd.FlagVersionName,
50+
Description: cmd.FlagVersionDesc,
51+
Dst: &c.serviceVersion.Value,
52+
Required: true,
53+
})
54+
55+
// Optional.
56+
c.RegisterFlagBool(c.jsonFlag()) // --json
57+
58+
return &c
59+
}
60+
61+
// Exec invokes the application logic for the command.
62+
func (c *DescribeCommand) Exec(_ io.Reader, out io.Writer) error {
63+
if c.Globals.Verbose() && c.jsonOutput.enabled {
64+
return fsterr.ErrInvalidVerboseJSONCombo
65+
}
66+
67+
serviceID, source, flag, err := cmd.ServiceID(cmd.OptionalServiceNameID{}, c.manifest, c.Globals.APIClient, c.Globals.ErrLog)
68+
if err != nil {
69+
return err
70+
}
71+
if c.Globals.Verbose() {
72+
cmd.DisplayServiceID(serviceID, flag, source, out)
73+
}
74+
75+
serviceVersion, err := c.serviceVersion.Parse(serviceID, c.Globals.APIClient)
76+
if err != nil {
77+
return err
78+
}
79+
80+
c.input.ServiceID = serviceID
81+
c.input.ServiceVersion = serviceVersion.Number
82+
83+
resource, err := c.Globals.APIClient.GetResource(&c.input)
84+
if err != nil {
85+
c.Globals.ErrLog.AddWithContext(err, map[string]any{
86+
"ID": c.input.ResourceID,
87+
"Service ID": c.input.ServiceID,
88+
"Service Version": c.input.ServiceVersion,
89+
})
90+
return err
91+
}
92+
93+
if ok, err := c.WriteJSON(out, resource); ok {
94+
return err
95+
}
96+
97+
if !c.Globals.Verbose() {
98+
text.Output(out, "Service ID: %s", resource.ServiceID)
99+
}
100+
text.Output(out, "Service Version: %s", resource.ServiceVersion)
101+
text.PrintResource(out, "", resource)
102+
103+
return nil
104+
}

0 commit comments

Comments
 (0)