Skip to content

Commit 14a9576

Browse files
authored
Auto import kubernetes template in Helm charts (#3550)
1 parent 94e96fa commit 14a9576

File tree

15 files changed

+422
-40
lines changed

15 files changed

+422
-40
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ dist/
3434
site/out/
3535

3636
*.tfstate
37+
*.tfstate.backup
3738
*.tfplan
3839
*.lock.hcl
3940
.terraform/

cli/server.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ func Server(newAPI func(*coderd.Options) *coderd.API) *cobra.Command {
108108
trace bool
109109
secureAuthCookie bool
110110
sshKeygenAlgorithmRaw string
111+
autoImportTemplates []string
111112
spooky bool
112113
verbose bool
113114
)
@@ -284,6 +285,28 @@ func Server(newAPI func(*coderd.Options) *coderd.API) *cobra.Command {
284285
URLs: []string{stunServer},
285286
})
286287
}
288+
289+
// Validate provided auto-import templates.
290+
var (
291+
validatedAutoImportTemplates = make([]coderd.AutoImportTemplate, len(autoImportTemplates))
292+
seenValidatedAutoImportTemplates = make(map[coderd.AutoImportTemplate]struct{}, len(autoImportTemplates))
293+
)
294+
for i, autoImportTemplate := range autoImportTemplates {
295+
var v coderd.AutoImportTemplate
296+
switch autoImportTemplate {
297+
case "kubernetes":
298+
v = coderd.AutoImportTemplateKubernetes
299+
default:
300+
return xerrors.Errorf("auto import template %q is not supported", autoImportTemplate)
301+
}
302+
303+
if _, ok := seenValidatedAutoImportTemplates[v]; ok {
304+
return xerrors.Errorf("auto import template %q is specified more than once", v)
305+
}
306+
seenValidatedAutoImportTemplates[v] = struct{}{}
307+
validatedAutoImportTemplates[i] = v
308+
}
309+
287310
options := &coderd.Options{
288311
AccessURL: accessURLParsed,
289312
ICEServers: iceServers,
@@ -297,6 +320,7 @@ func Server(newAPI func(*coderd.Options) *coderd.API) *cobra.Command {
297320
TURNServer: turnServer,
298321
TracerProvider: tracerProvider,
299322
Telemetry: telemetry.NewNoop(),
323+
AutoImportTemplates: validatedAutoImportTemplates,
300324
}
301325

302326
if oauth2GithubClientSecret != "" {
@@ -744,6 +768,7 @@ func Server(newAPI func(*coderd.Options) *coderd.API) *cobra.Command {
744768
cliflag.BoolVarP(root.Flags(), &secureAuthCookie, "secure-auth-cookie", "", "CODER_SECURE_AUTH_COOKIE", false, "Specifies if the 'Secure' property is set on browser session cookies")
745769
cliflag.StringVarP(root.Flags(), &sshKeygenAlgorithmRaw, "ssh-keygen-algorithm", "", "CODER_SSH_KEYGEN_ALGORITHM", "ed25519", "Specifies the algorithm to use for generating ssh keys. "+
746770
`Accepted values are "ed25519", "ecdsa", or "rsa4096"`)
771+
cliflag.StringArrayVarP(root.Flags(), &autoImportTemplates, "auto-import-template", "", "CODER_TEMPLATE_AUTOIMPORT", []string{}, "Which templates to auto-import. Available auto-importable templates are: kubernetes")
747772
cliflag.BoolVarP(root.Flags(), &spooky, "spooky", "", "", false, "Specifies spookiness level")
748773
cliflag.BoolVarP(root.Flags(), &verbose, "verbose", "v", "CODER_VERBOSE", false, "Enables verbose logging.")
749774
_ = root.Flags().MarkHidden("spooky")

coderd/coderd.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ type Options struct {
6666
Telemetry telemetry.Reporter
6767
TURNServer *turnconn.Server
6868
TracerProvider *sdktrace.TracerProvider
69+
AutoImportTemplates []AutoImportTemplate
6970
LicenseHandler http.Handler
7071
}
7172

coderd/coderdtest/coderdtest.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ type Options struct {
6868
GoogleTokenValidator *idtoken.Validator
6969
SSHKeygenAlgorithm gitsshkey.Algorithm
7070
APIRateLimit int
71+
AutoImportTemplates []coderd.AutoImportTemplate
7172
AutobuildTicker <-chan time.Time
7273
AutobuildStats chan<- executor.Stats
7374

@@ -210,6 +211,7 @@ func newWithAPI(t *testing.T, options *Options) (*codersdk.Client, io.Closer, *c
210211
APIRateLimit: options.APIRateLimit,
211212
Authorizer: options.Authorizer,
212213
Telemetry: telemetry.NewNoop(),
214+
AutoImportTemplates: options.AutoImportTemplates,
213215
})
214216
t.Cleanup(func() {
215217
_ = coderAPI.Close()

coderd/templates.go

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,17 @@ package coderd
22

33
import (
44
"context"
5+
"crypto/sha256"
56
"database/sql"
7+
"encoding/hex"
68
"errors"
79
"fmt"
810
"net/http"
911
"time"
1012

1113
"github.com/go-chi/chi/v5"
1214
"github.com/google/uuid"
15+
"github.com/moby/moby/pkg/namesgenerator"
1316
"golang.org/x/xerrors"
1417

1518
"github.com/coder/coder/coderd/database"
@@ -26,6 +29,14 @@ var (
2629
minAutostartIntervalDefault = time.Hour
2730
)
2831

32+
// Auto-importable templates. These can be auto-imported after the first user
33+
// has been created.
34+
type AutoImportTemplate string
35+
36+
const (
37+
AutoImportTemplateKubernetes AutoImportTemplate = "kubernetes"
38+
)
39+
2940
// Returns a single template.
3041
func (api *API) template(rw http.ResponseWriter, r *http.Request) {
3142
template := httpmw.TemplateParam(r)
@@ -508,6 +519,146 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) {
508519
httpapi.Write(rw, http.StatusOK, convertTemplate(updated, count, createdByNameMap[updated.ID.String()]))
509520
}
510521

522+
type autoImportTemplateOpts struct {
523+
name string
524+
archive []byte
525+
params map[string]string
526+
userID uuid.UUID
527+
orgID uuid.UUID
528+
}
529+
530+
func (api *API) autoImportTemplate(ctx context.Context, opts autoImportTemplateOpts) (database.Template, error) {
531+
var template database.Template
532+
err := api.Database.InTx(func(s database.Store) error {
533+
// Insert the archive into the files table.
534+
var (
535+
hash = sha256.Sum256(opts.archive)
536+
now = database.Now()
537+
)
538+
file, err := s.InsertFile(ctx, database.InsertFileParams{
539+
Hash: hex.EncodeToString(hash[:]),
540+
CreatedAt: now,
541+
CreatedBy: opts.userID,
542+
Mimetype: "application/x-tar",
543+
Data: opts.archive,
544+
})
545+
if err != nil {
546+
return xerrors.Errorf("insert auto-imported template archive into files table: %w", err)
547+
}
548+
549+
jobID := uuid.New()
550+
551+
// Insert parameters
552+
for key, value := range opts.params {
553+
_, err = s.InsertParameterValue(ctx, database.InsertParameterValueParams{
554+
ID: uuid.New(),
555+
Name: key,
556+
CreatedAt: now,
557+
UpdatedAt: now,
558+
Scope: database.ParameterScopeImportJob,
559+
ScopeID: jobID,
560+
SourceScheme: database.ParameterSourceSchemeData,
561+
SourceValue: value,
562+
DestinationScheme: database.ParameterDestinationSchemeProvisionerVariable,
563+
})
564+
if err != nil {
565+
return xerrors.Errorf("insert job-scoped parameter %q with value %q: %w", key, value, err)
566+
}
567+
}
568+
569+
// Create provisioner job
570+
job, err := s.InsertProvisionerJob(ctx, database.InsertProvisionerJobParams{
571+
ID: jobID,
572+
CreatedAt: now,
573+
UpdatedAt: now,
574+
OrganizationID: opts.orgID,
575+
InitiatorID: opts.userID,
576+
Provisioner: database.ProvisionerTypeTerraform,
577+
StorageMethod: database.ProvisionerStorageMethodFile,
578+
StorageSource: file.Hash,
579+
Type: database.ProvisionerJobTypeTemplateVersionImport,
580+
Input: []byte{'{', '}'},
581+
})
582+
if err != nil {
583+
return xerrors.Errorf("insert provisioner job: %w", err)
584+
}
585+
586+
// Create template version
587+
templateVersion, err := s.InsertTemplateVersion(ctx, database.InsertTemplateVersionParams{
588+
ID: uuid.New(),
589+
TemplateID: uuid.NullUUID{
590+
UUID: uuid.Nil,
591+
Valid: false,
592+
},
593+
OrganizationID: opts.orgID,
594+
CreatedAt: now,
595+
UpdatedAt: now,
596+
Name: namesgenerator.GetRandomName(1),
597+
Readme: "",
598+
JobID: job.ID,
599+
CreatedBy: uuid.NullUUID{
600+
UUID: opts.userID,
601+
Valid: true,
602+
},
603+
})
604+
if err != nil {
605+
return xerrors.Errorf("insert template version: %w", err)
606+
}
607+
608+
// Create template
609+
template, err = s.InsertTemplate(ctx, database.InsertTemplateParams{
610+
ID: uuid.New(),
611+
CreatedAt: now,
612+
UpdatedAt: now,
613+
OrganizationID: opts.orgID,
614+
Name: opts.name,
615+
Provisioner: job.Provisioner,
616+
ActiveVersionID: templateVersion.ID,
617+
Description: "This template was auto-imported by Coder.",
618+
MaxTtl: int64(maxTTLDefault),
619+
MinAutostartInterval: int64(minAutostartIntervalDefault),
620+
CreatedBy: opts.userID,
621+
})
622+
if err != nil {
623+
return xerrors.Errorf("insert template: %w", err)
624+
}
625+
626+
// Update template version with template ID
627+
err = s.UpdateTemplateVersionByID(ctx, database.UpdateTemplateVersionByIDParams{
628+
ID: templateVersion.ID,
629+
TemplateID: uuid.NullUUID{
630+
UUID: template.ID,
631+
Valid: true,
632+
},
633+
})
634+
if err != nil {
635+
return xerrors.Errorf("update template version to set template ID: %s", err)
636+
}
637+
638+
// Insert parameters at the template scope
639+
for key, value := range opts.params {
640+
_, err = s.InsertParameterValue(ctx, database.InsertParameterValueParams{
641+
ID: uuid.New(),
642+
Name: key,
643+
CreatedAt: now,
644+
UpdatedAt: now,
645+
Scope: database.ParameterScopeTemplate,
646+
ScopeID: template.ID,
647+
SourceScheme: database.ParameterSourceSchemeData,
648+
SourceValue: value,
649+
DestinationScheme: database.ParameterDestinationSchemeProvisionerVariable,
650+
})
651+
if err != nil {
652+
return xerrors.Errorf("insert template-scoped parameter %q with value %q: %w", key, value, err)
653+
}
654+
}
655+
656+
return nil
657+
})
658+
659+
return template, err
660+
}
661+
511662
func getCreatedByNamesByTemplateIDs(ctx context.Context, db database.Store, templates []database.Template) (map[string]string, error) {
512663
creators := make(map[string]string, len(templates))
513664
for _, template := range templates {

coderd/users.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package coderd
22

33
import (
4+
"bytes"
45
"context"
56
"crypto/sha256"
67
"database/sql"
@@ -9,6 +10,7 @@ import (
910
"net"
1011
"net/http"
1112
"net/url"
13+
"os"
1214
"strings"
1315
"time"
1416

@@ -18,6 +20,8 @@ import (
1820
"github.com/tabbed/pqtype"
1921
"golang.org/x/xerrors"
2022

23+
"cdr.dev/slog"
24+
2125
"github.com/coder/coder/coderd/database"
2226
"github.com/coder/coder/coderd/gitsshkey"
2327
"github.com/coder/coder/coderd/httpapi"
@@ -27,6 +31,7 @@ import (
2731
"github.com/coder/coder/coderd/userpassword"
2832
"github.com/coder/coder/codersdk"
2933
"github.com/coder/coder/cryptorand"
34+
"github.com/coder/coder/examples"
3035
)
3136

3237
// Returns whether the initial user has been created or not.
@@ -82,6 +87,8 @@ func (api *API) postFirstUser(rw http.ResponseWriter, r *http.Request) {
8287
Email: createUser.Email,
8388
Username: createUser.Username,
8489
Password: createUser.Password,
90+
// Create an org for the first user.
91+
OrganizationID: uuid.Nil,
8592
},
8693
LoginType: database.LoginTypePassword,
8794
})
@@ -116,6 +123,60 @@ func (api *API) postFirstUser(rw http.ResponseWriter, r *http.Request) {
116123
return
117124
}
118125

126+
// Auto-import any designated templates into the new organization.
127+
for _, template := range api.AutoImportTemplates {
128+
archive, err := examples.Archive(string(template))
129+
if err != nil {
130+
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
131+
Message: "Internal error importing template.",
132+
Detail: xerrors.Errorf("load template archive for %q: %w", template, err).Error(),
133+
})
134+
return
135+
}
136+
137+
// Determine which parameter values to use.
138+
parameters := map[string]string{}
139+
switch template {
140+
case AutoImportTemplateKubernetes:
141+
142+
// Determine the current namespace we're in.
143+
const namespaceFile = "/var/run/secrets/kubernetes.io/serviceaccount/namespace"
144+
namespace, err := os.ReadFile(namespaceFile)
145+
if err != nil {
146+
parameters["use_kubeconfig"] = "true" // use ~/.config/kubeconfig
147+
parameters["namespace"] = "coder-workspaces"
148+
} else {
149+
parameters["use_kubeconfig"] = "false" // use SA auth
150+
parameters["namespace"] = string(bytes.TrimSpace(namespace))
151+
}
152+
153+
default:
154+
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
155+
Message: "Internal error importing template.",
156+
Detail: fmt.Sprintf("cannot auto-import %q template", template),
157+
})
158+
return
159+
}
160+
161+
tpl, err := api.autoImportTemplate(r.Context(), autoImportTemplateOpts{
162+
name: string(template),
163+
archive: archive,
164+
params: parameters,
165+
userID: user.ID,
166+
orgID: organizationID,
167+
})
168+
if err != nil {
169+
api.Logger.Warn(r.Context(), "failed to auto-import template", slog.F("template", template), slog.F("parameters", parameters), slog.Error(err))
170+
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
171+
Message: "Internal error importing template.",
172+
Detail: xerrors.Errorf("failed to import template %q: %w", template, err).Error(),
173+
})
174+
return
175+
}
176+
177+
api.Logger.Info(r.Context(), "auto-imported template", slog.F("id", tpl.ID), slog.F("template", template), slog.F("parameters", parameters))
178+
}
179+
119180
httpapi.Write(rw, http.StatusCreated, codersdk.CreateFirstUserResponse{
120181
UserID: user.ID,
121182
OrganizationID: organizationID,

0 commit comments

Comments
 (0)