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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy