Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit9b3b641

Browse files
authored
feat: Add template pull cmd (#2329)
1 parenta6a06d4 commit9b3b641

File tree

7 files changed

+283
-11
lines changed

7 files changed

+283
-11
lines changed

‎cli/templatepull.go

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
package cli
2+
3+
import (
4+
"fmt"
5+
"io/fs"
6+
"os"
7+
"sort"
8+
9+
"github.com/spf13/cobra"
10+
"golang.org/x/xerrors"
11+
12+
"github.com/coder/coder/cli/cliui"
13+
"github.com/coder/coder/codersdk"
14+
)
15+
16+
functemplatePull()*cobra.Command {
17+
cmd:=&cobra.Command{
18+
Use:"pull <name> [destination]",
19+
Short:"Download the latest version of a template to a path.",
20+
Args:cobra.MaximumNArgs(2),
21+
RunE:func(cmd*cobra.Command,args []string)error {
22+
var (
23+
ctx=cmd.Context()
24+
templateName=args[0]
25+
deststring
26+
)
27+
28+
iflen(args)>1 {
29+
dest=args[1]
30+
}
31+
32+
client,err:=createClient(cmd)
33+
iferr!=nil {
34+
returnxerrors.Errorf("create client: %w",err)
35+
}
36+
37+
// TODO(JonA): Do we need to add a flag for organization?
38+
organization,err:=currentOrganization(cmd,client)
39+
iferr!=nil {
40+
returnxerrors.Errorf("current organization: %w",err)
41+
}
42+
43+
template,err:=client.TemplateByName(ctx,organization.ID,templateName)
44+
iferr!=nil {
45+
returnxerrors.Errorf("template by name: %w",err)
46+
}
47+
48+
// Pull the versions for the template. We'll find the latest
49+
// one and download the source.
50+
versions,err:=client.TemplateVersionsByTemplate(ctx, codersdk.TemplateVersionsByTemplateRequest{
51+
TemplateID:template.ID,
52+
})
53+
iferr!=nil {
54+
returnxerrors.Errorf("template versions by template: %w",err)
55+
}
56+
57+
iflen(versions)==0 {
58+
returnxerrors.Errorf("no template versions for template %q",templateName)
59+
}
60+
61+
// Sort the slice from newest to oldest template.
62+
sort.SliceStable(versions,func(i,jint)bool {
63+
returnversions[i].CreatedAt.After(versions[j].CreatedAt)
64+
})
65+
66+
latest:=versions[0]
67+
68+
// Download the tar archive.
69+
raw,ctype,err:=client.Download(ctx,latest.Job.StorageSource)
70+
iferr!=nil {
71+
returnxerrors.Errorf("download template: %w",err)
72+
}
73+
74+
ifctype!=codersdk.ContentTypeTar {
75+
returnxerrors.Errorf("unexpected Content-Type %q, expecting %q",ctype,codersdk.ContentTypeTar)
76+
}
77+
78+
// If the destination is empty then we write to stdout
79+
// and bail early.
80+
ifdest=="" {
81+
_,err=cmd.OutOrStdout().Write(raw)
82+
iferr!=nil {
83+
returnxerrors.Errorf("write stdout: %w",err)
84+
}
85+
returnnil
86+
}
87+
88+
// Stat the destination to ensure nothing exists already.
89+
fi,err:=os.Stat(dest)
90+
iferr!=nil&&!xerrors.Is(err,fs.ErrNotExist) {
91+
returnxerrors.Errorf("stat destination: %w",err)
92+
}
93+
94+
iffi!=nil&&fi.IsDir() {
95+
// If the destination is a directory we just bail.
96+
returnxerrors.Errorf("%q already exists.",dest)
97+
}
98+
99+
// If a file exists at the destination prompt the user
100+
// to ensure we don't overwrite something valuable.
101+
iffi!=nil {
102+
_,err=cliui.Prompt(cmd, cliui.PromptOptions{
103+
Text:fmt.Sprintf("%q already exists, do you want to overwrite it?",dest),
104+
IsConfirm:true,
105+
})
106+
iferr!=nil {
107+
returnxerrors.Errorf("parse prompt: %w",err)
108+
}
109+
}
110+
111+
err=os.WriteFile(dest,raw,0600)
112+
iferr!=nil {
113+
returnxerrors.Errorf("write to path: %w",err)
114+
}
115+
116+
returnnil
117+
},
118+
}
119+
120+
cliui.AllowSkipPrompt(cmd)
121+
122+
returncmd
123+
}

‎cli/templatepull_test.go

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
package cli_test
2+
3+
import (
4+
"bytes"
5+
"os"
6+
"path/filepath"
7+
"testing"
8+
9+
"github.com/google/uuid"
10+
"github.com/stretchr/testify/require"
11+
12+
"github.com/coder/coder/cli/clitest"
13+
"github.com/coder/coder/coderd/coderdtest"
14+
"github.com/coder/coder/provisioner/echo"
15+
"github.com/coder/coder/provisionersdk/proto"
16+
"github.com/coder/coder/pty/ptytest"
17+
)
18+
19+
funcTestTemplatePull(t*testing.T) {
20+
t.Parallel()
21+
22+
// Stdout tests that 'templates pull' pulls down the latest template
23+
// and writes it to stdout.
24+
t.Run("Stdout",func(t*testing.T) {
25+
t.Parallel()
26+
27+
client:=coderdtest.New(t,&coderdtest.Options{IncludeProvisionerD:true})
28+
user:=coderdtest.CreateFirstUser(t,client)
29+
30+
// Create an initial template bundle.
31+
source1:=genTemplateVersionSource()
32+
// Create an updated template bundle. This will be used to ensure
33+
// that templates are correctly returned in order from latest to oldest.
34+
source2:=genTemplateVersionSource()
35+
36+
expected,err:=echo.Tar(source2)
37+
require.NoError(t,err)
38+
39+
version1:=coderdtest.CreateTemplateVersion(t,client,user.OrganizationID,source1)
40+
_=coderdtest.AwaitTemplateVersionJob(t,client,version1.ID)
41+
42+
template:=coderdtest.CreateTemplate(t,client,user.OrganizationID,version1.ID)
43+
44+
// Update the template version so that we can assert that templates
45+
// are being sorted correctly.
46+
_=coderdtest.UpdateTemplateVersion(t,client,user.OrganizationID,source2,template.ID)
47+
48+
cmd,root:=clitest.New(t,"templates","pull",template.Name)
49+
clitest.SetupConfig(t,client,root)
50+
51+
varbuf bytes.Buffer
52+
cmd.SetOut(&buf)
53+
54+
err=cmd.Execute()
55+
require.NoError(t,err)
56+
57+
require.True(t,bytes.Equal(expected,buf.Bytes()),"tar files differ")
58+
})
59+
60+
// ToFile tests that 'templates pull' pulls down the latest template
61+
// and writes it to the correct directory.
62+
t.Run("ToFile",func(t*testing.T) {
63+
t.Parallel()
64+
65+
client:=coderdtest.New(t,&coderdtest.Options{IncludeProvisionerD:true})
66+
user:=coderdtest.CreateFirstUser(t,client)
67+
68+
// Create an initial template bundle.
69+
source1:=genTemplateVersionSource()
70+
// Create an updated template bundle. This will be used to ensure
71+
// that templates are correctly returned in order from latest to oldest.
72+
source2:=genTemplateVersionSource()
73+
74+
expected,err:=echo.Tar(source2)
75+
require.NoError(t,err)
76+
77+
version1:=coderdtest.CreateTemplateVersion(t,client,user.OrganizationID,source1)
78+
_=coderdtest.AwaitTemplateVersionJob(t,client,version1.ID)
79+
80+
template:=coderdtest.CreateTemplate(t,client,user.OrganizationID,version1.ID)
81+
82+
// Update the template version so that we can assert that templates
83+
// are being sorted correctly.
84+
_=coderdtest.UpdateTemplateVersion(t,client,user.OrganizationID,source2,template.ID)
85+
86+
dir:=t.TempDir()
87+
88+
dest:=filepath.Join(dir,"actual.tar")
89+
90+
// Create the file so that we can test that the command
91+
// warns the user before overwriting a preexisting file.
92+
fi,err:=os.OpenFile(dest,os.O_CREATE|os.O_RDONLY,0600)
93+
require.NoError(t,err)
94+
_=fi.Close()
95+
96+
cmd,root:=clitest.New(t,"templates","pull",template.Name,dest)
97+
clitest.SetupConfig(t,client,root)
98+
99+
pty:=ptytest.New(t)
100+
cmd.SetIn(pty.Input())
101+
cmd.SetOut(pty.Output())
102+
103+
errChan:=make(chanerror)
104+
gofunc() {
105+
deferclose(errChan)
106+
errChan<-cmd.Execute()
107+
}()
108+
109+
// We expect to be prompted that a file already exists.
110+
pty.ExpectMatch("already exists")
111+
pty.WriteLine("yes")
112+
113+
require.NoError(t,<-errChan)
114+
115+
actual,err:=os.ReadFile(dest)
116+
require.NoError(t,err)
117+
118+
require.True(t,bytes.Equal(actual,expected),"tar files differ")
119+
})
120+
}
121+
122+
// genTemplateVersionSource returns a unique bundle that can be used to create
123+
// a template version source.
124+
funcgenTemplateVersionSource()*echo.Responses {
125+
return&echo.Responses{
126+
Parse: []*proto.Parse_Response{
127+
{
128+
Type:&proto.Parse_Response_Log{
129+
Log:&proto.Log{
130+
Output:uuid.NewString(),
131+
},
132+
},
133+
},
134+
135+
{
136+
Type:&proto.Parse_Response_Complete{
137+
Complete:&proto.Parse_Complete{},
138+
},
139+
},
140+
},
141+
Provision:echo.ProvisionComplete,
142+
}
143+
}

‎cli/templates.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ func templates() *cobra.Command {
3333
templateUpdate(),
3434
templateVersions(),
3535
templateDelete(),
36+
templatePull(),
3637
)
3738

3839
returncmd

‎coderd/provisionerjobs.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -287,9 +287,10 @@ func convertProvisionerJobLog(provisionerJobLog database.ProvisionerJobLog) code
287287

288288
funcconvertProvisionerJob(provisionerJob database.ProvisionerJob) codersdk.ProvisionerJob {
289289
job:= codersdk.ProvisionerJob{
290-
ID:provisionerJob.ID,
291-
CreatedAt:provisionerJob.CreatedAt,
292-
Error:provisionerJob.Error.String,
290+
ID:provisionerJob.ID,
291+
CreatedAt:provisionerJob.CreatedAt,
292+
Error:provisionerJob.Error.String,
293+
StorageSource:provisionerJob.StorageSource,
293294
}
294295
// Applying values optional to the struct.
295296
ifprovisionerJob.StartedAt.Valid {

‎codersdk/provisionerdaemons.go

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -60,13 +60,14 @@ const (
6060
)
6161

6262
typeProvisionerJobstruct {
63-
ID uuid.UUID`json:"id"`
64-
CreatedAt time.Time`json:"created_at"`
65-
StartedAt*time.Time`json:"started_at,omitempty"`
66-
CompletedAt*time.Time`json:"completed_at,omitempty"`
67-
Errorstring`json:"error,omitempty"`
68-
StatusProvisionerJobStatus`json:"status"`
69-
WorkerID*uuid.UUID`json:"worker_id,omitempty"`
63+
ID uuid.UUID`json:"id"`
64+
CreatedAt time.Time`json:"created_at"`
65+
StartedAt*time.Time`json:"started_at,omitempty"`
66+
CompletedAt*time.Time`json:"completed_at,omitempty"`
67+
Errorstring`json:"error,omitempty"`
68+
StatusProvisionerJobStatus`json:"status"`
69+
WorkerID*uuid.UUID`json:"worker_id,omitempty"`
70+
StorageSourcestring`json:"storage_source"`
7071
}
7172

7273
typeProvisionerJobLogstruct {

‎site/src/api/typesGenerated.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,9 +211,10 @@ export interface ProvisionerJob {
211211
readonlyerror?:string
212212
readonlystatus:ProvisionerJobStatus
213213
readonlyworker_id?:string
214+
readonlystorage_source:string
214215
}
215216

216-
// From codersdk/provisionerdaemons.go:72:6
217+
// From codersdk/provisionerdaemons.go:73:6
217218
exportinterfaceProvisionerJobLog{
218219
readonlyid:string
219220
readonlycreated_at:string

‎site/src/testHelpers/entities.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,9 @@ export const MockProvisionerJob: TypesGen.ProvisionerJob = {
7979
created_at:"",
8080
id:"test-provisioner-job",
8181
status:"succeeded",
82+
storage_source:"asdf",
8283
}
84+
8385
exportconstMockFailedProvisionerJob:TypesGen.ProvisionerJob={
8486
...MockProvisionerJob,
8587
status:"failed",

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp