Skip to content

Commit e7ea708

Browse files
authored
feat: add plural cd services tarball command (#661)
* add new command * parse args ang fetch deploy token * build tarball url * download and untar * add emptiness check * update default dir * fix linter
1 parent 6a21b9e commit e7ea708

File tree

2 files changed

+151
-14
lines changed

2 files changed

+151
-14
lines changed

cmd/command/cd/cd_services.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"strings"
77

88
"github.com/pluralsh/plural-cli/pkg/common"
9+
"github.com/pluralsh/polly/fs"
910
lua "github.com/yuin/gopher-lua"
1011

1112
gqlclient "github.com/pluralsh/console/go/client"
@@ -149,6 +150,18 @@ func (p *Plural) cdServiceCommands() []cli.Command {
149150
Action: common.LatestVersion(common.RequireArgs(p.handleKickClusterService, []string{"@{cluster-handle}/{serviceName}"})),
150151
Usage: "force sync cluster service",
151152
},
153+
{
154+
Name: "tarball",
155+
ArgsUsage: "@{cluster-handle}/{serviceName}",
156+
Action: common.LatestVersion(common.RequireArgs(p.handleTarballClusterService, []string{"@{cluster-handle}/{serviceName}"})),
157+
Usage: "download service tarball locally",
158+
Flags: []cli.Flag{
159+
cli.StringFlag{
160+
Name: "dir",
161+
Usage: "directory to download to, defaults to ./service-tarball",
162+
},
163+
},
164+
},
152165
}
153166
}
154167

@@ -627,6 +640,50 @@ func (p *Plural) handleKickClusterService(c *cli.Context) error {
627640
return nil
628641
}
629642

643+
func (p *Plural) handleTarballClusterService(c *cli.Context) error {
644+
serviceId, clusterName, serviceName, err := getServiceIdClusterNameServiceName(c.Args().Get(0))
645+
if err != nil {
646+
return fmt.Errorf("could not parse args: %w", err)
647+
}
648+
649+
if err = p.InitConsoleClient(consoleToken, consoleURL); err != nil {
650+
return fmt.Errorf("could not initialize console client: %w", err)
651+
}
652+
653+
service, err := p.ConsoleClient.GetClusterService(serviceId, serviceName, clusterName)
654+
if err != nil {
655+
return fmt.Errorf("could not get service: %w", err)
656+
}
657+
if service == nil {
658+
return fmt.Errorf("could not get service for: %s", c.Args().Get(0))
659+
}
660+
if service.Tarball == nil {
661+
return fmt.Errorf("service %s does not have a tarball", service.Name)
662+
}
663+
664+
dir := c.String("dir")
665+
if dir == "" {
666+
dir = filepath.Join(".", service.Name+"-tarball")
667+
}
668+
if err = utils.EnsureEmptyDir(dir); err != nil {
669+
return fmt.Errorf("could not ensure dir: %w", err)
670+
}
671+
672+
deployToken, err := p.ConsoleClient.GetDeployToken(&service.Cluster.ID, nil)
673+
if err != nil {
674+
return fmt.Errorf("could not get deploy token: %w", err)
675+
}
676+
677+
utils.Highlight("fetching tarball from %s\n", *service.Tarball)
678+
resp, err := utils.ReadRemoteFileWithRetries(*service.Tarball, deployToken, 3)
679+
if err != nil {
680+
return err
681+
}
682+
defer resp.Close()
683+
684+
return fs.Untar(dir, resp)
685+
}
686+
630687
type ServiceDeploymentAttributesConfiguration struct {
631688
Configuration []*gqlclient.ConfigAttributes
632689
}

pkg/utils/file.go

Lines changed: 94 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"os"
1111
"path"
1212
"path/filepath"
13+
"time"
1314

1415
"github.com/pluralsh/plural-cli/pkg/utils/pathing"
1516
"sigs.k8s.io/yaml"
@@ -66,20 +67,6 @@ func EmptyDirectory(dir string) error {
6667
return nil
6768
}
6869

69-
func IsEmpty(name string) (bool, error) {
70-
f, err := os.Open(name)
71-
if err != nil {
72-
return false, err
73-
}
74-
defer f.Close()
75-
76-
_, err = f.Readdirnames(1) // Or f.Readdir(1)
77-
if errors.Is(err, io.EOF) {
78-
return true, nil
79-
}
80-
return false, err // Either not empty or error, suits both cases
81-
}
82-
8370
func WriteFile(name string, content []byte) error {
8471
if err := os.MkdirAll(filepath.Dir(name), 0755); err != nil {
8572
return err
@@ -106,6 +93,52 @@ func ReadRemoteFile(url string) (string, error) {
10693
return buffer.String(), nil
10794
}
10895

96+
func ReadRemoteFileWithRetries(url, token string, retries int) (io.ReadCloser, error) {
97+
req, err := http.NewRequest(http.MethodGet, url, nil)
98+
if err != nil {
99+
return nil, err
100+
}
101+
req.Header.Add("Authorization", "Token "+token)
102+
103+
for i := 0; i < retries; i++ {
104+
resp, retriable, err := doRequest(req)
105+
if err != nil {
106+
if !retriable {
107+
return nil, err
108+
}
109+
110+
time.Sleep(time.Duration(50*(i+1)) * time.Millisecond)
111+
continue
112+
}
113+
114+
return resp, nil
115+
}
116+
117+
return nil, fmt.Errorf("could read file, retries exhaused: %w", err)
118+
}
119+
120+
func doRequest(req *http.Request) (io.ReadCloser, bool, error) {
121+
client := &http.Client{Timeout: time.Minute, Transport: &http.Transport{ResponseHeaderTimeout: time.Minute}}
122+
123+
resp, err := client.Do(req)
124+
if err != nil {
125+
return nil, false, err
126+
}
127+
128+
if resp.StatusCode != http.StatusOK {
129+
defer resp.Body.Close()
130+
errMsg, err := io.ReadAll(resp.Body)
131+
if err != nil {
132+
return nil, false, fmt.Errorf("could not read response body: %w", err)
133+
}
134+
135+
return nil, resp.StatusCode == http.StatusTooManyRequests,
136+
fmt.Errorf("could not fetch url: %s, error: %s, code: %d", req.URL.String(), string(errMsg), resp.StatusCode)
137+
}
138+
139+
return resp.Body, false, nil
140+
}
141+
109142
func YamlFile(name string, out interface{}) error {
110143
content, err := os.ReadFile(name)
111144
if err != nil {
@@ -195,3 +228,50 @@ func CopyDir(src string, dst string) error {
195228
}
196229
return nil
197230
}
231+
232+
func EnsureDir(dir string) error {
233+
if dir == "" {
234+
return fmt.Errorf("directory name cannot be empty")
235+
}
236+
237+
if !Exists(dir) {
238+
return os.MkdirAll(dir, 0755)
239+
}
240+
241+
if !IsDir(dir) {
242+
return fmt.Errorf("%s is not a directory", dir)
243+
}
244+
245+
return nil
246+
}
247+
248+
func EnsureEmptyDir(dir string) error {
249+
if err := EnsureDir(dir); err != nil {
250+
return err
251+
}
252+
253+
empty, err := IsEmptyDir(dir)
254+
if err != nil {
255+
return fmt.Errorf("could not check if directory %s is empty: %w", dir, err)
256+
}
257+
258+
if !empty {
259+
return fmt.Errorf("directory %s is not empty", dir)
260+
}
261+
262+
return nil
263+
}
264+
265+
func IsEmptyDir(name string) (bool, error) {
266+
f, err := os.Open(name)
267+
if err != nil {
268+
return false, err
269+
}
270+
defer f.Close()
271+
272+
_, err = f.Readdirnames(1) // Or f.Readdir(1)
273+
if errors.Is(err, io.EOF) {
274+
return true, nil
275+
}
276+
return false, err // Either not empty or error, suits both cases
277+
}

0 commit comments

Comments
 (0)