Skip to content

Commit e8ad3d4

Browse files
committed
feat: Add template pull cmd
1 parent 0a949aa commit e8ad3d4

File tree

6 files changed

+182
-3
lines changed

6 files changed

+182
-3
lines changed

cli/templatepull.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package cli
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"path/filepath"
7+
8+
"github.com/spf13/cobra"
9+
"golang.org/x/xerrors"
10+
11+
"github.com/coder/coder/codersdk"
12+
)
13+
14+
func templatePull() *cobra.Command {
15+
cmd := &cobra.Command{
16+
Use: "pull <name> [destination]",
17+
Short: "Download the latest version of a template to a path.",
18+
Args: cobra.MaximumNArgs(2),
19+
RunE: func(cmd *cobra.Command, args []string) error {
20+
var (
21+
ctx = cmd.Context()
22+
templateName = args[0]
23+
dest string
24+
)
25+
26+
if len(args) > 1 {
27+
dest = args[1]
28+
}
29+
30+
client, err := createClient(cmd)
31+
if err != nil {
32+
return xerrors.Errorf("create client: %w", err)
33+
}
34+
35+
// TODO(JonA): Do we need to add a flag for organization?
36+
organization, err := currentOrganization(cmd, client)
37+
if err != nil {
38+
return xerrors.Errorf("current organization: %w", err)
39+
}
40+
41+
template, err := client.TemplateByName(ctx, organization.ID, templateName)
42+
if err != nil {
43+
return xerrors.Errorf("template by name: %w", err)
44+
}
45+
46+
versions, err := client.TemplateVersionsByTemplate(ctx, codersdk.TemplateVersionsByTemplateRequest{
47+
TemplateID: template.ID,
48+
})
49+
if err != nil {
50+
return xerrors.Errorf("template versions by template: %w", err)
51+
}
52+
53+
if len(versions) == 0 {
54+
return xerrors.Errorf("no template versions for template %q", templateName)
55+
}
56+
57+
// TemplateVersionsByTemplate returns the versions in order from newest
58+
// to oldest.
59+
latest := versions[0]
60+
61+
raw, ctype, err := client.Download(ctx, latest.Job.SourceHash)
62+
if err != nil {
63+
return xerrors.Errorf("download template: %w", err)
64+
}
65+
66+
if ctype != codersdk.ContentTypeTar {
67+
return xerrors.Errorf("unexpected Content-Type %q, expecting %q", ctype, codersdk.ContentTypeTar)
68+
}
69+
70+
if dest == "" {
71+
_, err = cmd.OutOrStdout().Write(raw)
72+
if err != nil {
73+
return xerrors.Errorf("write stdout: %w", err)
74+
}
75+
return nil
76+
}
77+
78+
name := fmt.Sprintf("%s.tar", templateName)
79+
err = os.WriteFile(filepath.Join(dest, name), raw, 0600)
80+
if err != nil {
81+
return xerrors.Errorf("write to path: %w", err)
82+
}
83+
84+
// TODO(Handle '~')
85+
86+
return nil
87+
},
88+
}
89+
90+
return cmd
91+
}

cli/templatepull_test.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package cli_test
2+
3+
import (
4+
"bytes"
5+
"testing"
6+
7+
"github.com/stretchr/testify/require"
8+
9+
"github.com/coder/coder/cli/clitest"
10+
"github.com/coder/coder/coderd/coderdtest"
11+
"github.com/coder/coder/provisioner/echo"
12+
"github.com/coder/coder/provisionersdk/proto"
13+
"github.com/coder/coder/pty/ptytest"
14+
)
15+
16+
func TestTemplatePull(t *testing.T) {
17+
t.Parallel()
18+
19+
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true})
20+
user := coderdtest.CreateFirstUser(t, client)
21+
22+
templateSource := &echo.Responses{
23+
Parse: []*proto.Parse_Response{
24+
{
25+
Type: &proto.Parse_Response_Log{
26+
Log: &proto.Log{Output: "yahoo"},
27+
},
28+
},
29+
30+
{
31+
Type: &proto.Parse_Response_Complete{
32+
Complete: &proto.Parse_Complete{},
33+
},
34+
},
35+
},
36+
Provision: echo.ProvisionComplete,
37+
}
38+
39+
templateSource2 := &echo.Responses{
40+
Parse: []*proto.Parse_Response{
41+
{
42+
Type: &proto.Parse_Response_Log{
43+
Log: &proto.Log{Output: "wahoo"},
44+
},
45+
},
46+
47+
{
48+
Type: &proto.Parse_Response_Complete{
49+
Complete: &proto.Parse_Complete{},
50+
},
51+
},
52+
},
53+
Provision: echo.ProvisionComplete,
54+
}
55+
56+
expected, err := echo.Tar(templateSource2)
57+
require.NoError(t, err)
58+
59+
version1 := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, templateSource)
60+
_ = coderdtest.AwaitTemplateVersionJob(t, client, version1.ID)
61+
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version1.ID)
62+
63+
_ = coderdtest.UpdateTemplateVersion(t, client, user.OrganizationID, templateSource2, template.ID)
64+
65+
cmd, root := clitest.New(t, "templates", "pull", template.Name)
66+
clitest.SetupConfig(t, client, root)
67+
68+
buf := &bytes.Buffer{}
69+
pty := ptytest.New(t)
70+
cmd.SetOut(buf)
71+
72+
err = cmd.Execute()
73+
require.NoError(t, err)
74+
75+
err = pty.Close()
76+
require.NoError(t, err)
77+
78+
require.True(t, bytes.Equal(expected, buf.Bytes()), "Bytes differ")
79+
}

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
return cmd

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
func convertProvisionerJob(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+
SourceHash: provisionerJob.StorageSource,
293294
}
294295
// Applying values optional to the struct.
295296
if provisionerJob.StartedAt.Valid {

coderd/templateversions.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"errors"
77
"fmt"
88
"net/http"
9+
"sort"
910

1011
"github.com/go-chi/chi/v5"
1112
"github.com/google/uuid"
@@ -473,6 +474,11 @@ func (api *API) templateVersionsByTemplate(rw http.ResponseWriter, r *http.Reque
473474
return
474475
}
475476

477+
// Sort the slice from newest to oldest template.
478+
sort.SliceStable(apiVersions, func(i, j int) bool {
479+
return apiVersions[i].CreatedAt.After(apiVersions[j].CreatedAt)
480+
})
481+
476482
httpapi.Write(rw, http.StatusOK, apiVersions)
477483
}
478484

codersdk/provisionerdaemons.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ type ProvisionerJob struct {
6767
Error string `json:"error,omitempty"`
6868
Status ProvisionerJobStatus `json:"status"`
6969
WorkerID *uuid.UUID `json:"worker_id,omitempty"`
70+
SourceHash string
7071
}
7172

7273
type ProvisionerJobLog struct {

0 commit comments

Comments
 (0)