Skip to content

Auto import kubernetes template in Helm charts #3550

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Aug 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ dist/
site/out/

*.tfstate
*.tfstate.backup
*.tfplan
*.lock.hcl
.terraform/
Expand Down
25 changes: 25 additions & 0 deletions cli/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ func Server(newAPI func(*coderd.Options) *coderd.API) *cobra.Command {
trace bool
secureAuthCookie bool
sshKeygenAlgorithmRaw string
autoImportTemplates []string
spooky bool
verbose bool
)
Expand Down Expand Up @@ -284,6 +285,28 @@ func Server(newAPI func(*coderd.Options) *coderd.API) *cobra.Command {
URLs: []string{stunServer},
})
}

// Validate provided auto-import templates.
var (
validatedAutoImportTemplates = make([]coderd.AutoImportTemplate, len(autoImportTemplates))
seenValidatedAutoImportTemplates = make(map[coderd.AutoImportTemplate]struct{}, len(autoImportTemplates))
)
for i, autoImportTemplate := range autoImportTemplates {
var v coderd.AutoImportTemplate
switch autoImportTemplate {
case "kubernetes":
v = coderd.AutoImportTemplateKubernetes
default:
return xerrors.Errorf("auto import template %q is not supported", autoImportTemplate)
}

if _, ok := seenValidatedAutoImportTemplates[v]; ok {
return xerrors.Errorf("auto import template %q is specified more than once", v)
}
seenValidatedAutoImportTemplates[v] = struct{}{}
validatedAutoImportTemplates[i] = v
}

options := &coderd.Options{
AccessURL: accessURLParsed,
ICEServers: iceServers,
Expand All @@ -297,6 +320,7 @@ func Server(newAPI func(*coderd.Options) *coderd.API) *cobra.Command {
TURNServer: turnServer,
TracerProvider: tracerProvider,
Telemetry: telemetry.NewNoop(),
AutoImportTemplates: validatedAutoImportTemplates,
}

if oauth2GithubClientSecret != "" {
Expand Down Expand Up @@ -744,6 +768,7 @@ func Server(newAPI func(*coderd.Options) *coderd.API) *cobra.Command {
cliflag.BoolVarP(root.Flags(), &secureAuthCookie, "secure-auth-cookie", "", "CODER_SECURE_AUTH_COOKIE", false, "Specifies if the 'Secure' property is set on browser session cookies")
cliflag.StringVarP(root.Flags(), &sshKeygenAlgorithmRaw, "ssh-keygen-algorithm", "", "CODER_SSH_KEYGEN_ALGORITHM", "ed25519", "Specifies the algorithm to use for generating ssh keys. "+
`Accepted values are "ed25519", "ecdsa", or "rsa4096"`)
cliflag.StringArrayVarP(root.Flags(), &autoImportTemplates, "auto-import-template", "", "CODER_TEMPLATE_AUTOIMPORT", []string{}, "Which templates to auto-import. Available auto-importable templates are: kubernetes")
cliflag.BoolVarP(root.Flags(), &spooky, "spooky", "", "", false, "Specifies spookiness level")
cliflag.BoolVarP(root.Flags(), &verbose, "verbose", "v", "CODER_VERBOSE", false, "Enables verbose logging.")
_ = root.Flags().MarkHidden("spooky")
Expand Down
1 change: 1 addition & 0 deletions coderd/coderd.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ type Options struct {
Telemetry telemetry.Reporter
TURNServer *turnconn.Server
TracerProvider *sdktrace.TracerProvider
AutoImportTemplates []AutoImportTemplate
LicenseHandler http.Handler
}

Expand Down
2 changes: 2 additions & 0 deletions coderd/coderdtest/coderdtest.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ type Options struct {
GoogleTokenValidator *idtoken.Validator
SSHKeygenAlgorithm gitsshkey.Algorithm
APIRateLimit int
AutoImportTemplates []coderd.AutoImportTemplate
AutobuildTicker <-chan time.Time
AutobuildStats chan<- executor.Stats

Expand Down Expand Up @@ -210,6 +211,7 @@ func newWithAPI(t *testing.T, options *Options) (*codersdk.Client, io.Closer, *c
APIRateLimit: options.APIRateLimit,
Authorizer: options.Authorizer,
Telemetry: telemetry.NewNoop(),
AutoImportTemplates: options.AutoImportTemplates,
})
t.Cleanup(func() {
_ = coderAPI.Close()
Expand Down
151 changes: 151 additions & 0 deletions coderd/templates.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@ package coderd

import (
"context"
"crypto/sha256"
"database/sql"
"encoding/hex"
"errors"
"fmt"
"net/http"
"time"

"github.com/go-chi/chi/v5"
"github.com/google/uuid"
"github.com/moby/moby/pkg/namesgenerator"
"golang.org/x/xerrors"

"github.com/coder/coder/coderd/database"
Expand All @@ -26,6 +29,14 @@ var (
minAutostartIntervalDefault = time.Hour
)

// Auto-importable templates. These can be auto-imported after the first user
// has been created.
type AutoImportTemplate string

const (
AutoImportTemplateKubernetes AutoImportTemplate = "kubernetes"
)

// Returns a single template.
func (api *API) template(rw http.ResponseWriter, r *http.Request) {
template := httpmw.TemplateParam(r)
Expand Down Expand Up @@ -508,6 +519,146 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) {
httpapi.Write(rw, http.StatusOK, convertTemplate(updated, count, createdByNameMap[updated.ID.String()]))
}

type autoImportTemplateOpts struct {
name string
archive []byte
params map[string]string
userID uuid.UUID
orgID uuid.UUID
}

func (api *API) autoImportTemplate(ctx context.Context, opts autoImportTemplateOpts) (database.Template, error) {
var template database.Template
err := api.Database.InTx(func(s database.Store) error {
// Insert the archive into the files table.
var (
hash = sha256.Sum256(opts.archive)
now = database.Now()
)
file, err := s.InsertFile(ctx, database.InsertFileParams{
Hash: hex.EncodeToString(hash[:]),
CreatedAt: now,
CreatedBy: opts.userID,
Mimetype: "application/x-tar",
Data: opts.archive,
})
if err != nil {
return xerrors.Errorf("insert auto-imported template archive into files table: %w", err)
}

jobID := uuid.New()

// Insert parameters
for key, value := range opts.params {
_, err = s.InsertParameterValue(ctx, database.InsertParameterValueParams{
ID: uuid.New(),
Name: key,
CreatedAt: now,
UpdatedAt: now,
Scope: database.ParameterScopeImportJob,
ScopeID: jobID,
SourceScheme: database.ParameterSourceSchemeData,
SourceValue: value,
DestinationScheme: database.ParameterDestinationSchemeProvisionerVariable,
})
if err != nil {
return xerrors.Errorf("insert job-scoped parameter %q with value %q: %w", key, value, err)
}
}

// Create provisioner job
job, err := s.InsertProvisionerJob(ctx, database.InsertProvisionerJobParams{
ID: jobID,
CreatedAt: now,
UpdatedAt: now,
OrganizationID: opts.orgID,
InitiatorID: opts.userID,
Provisioner: database.ProvisionerTypeTerraform,
StorageMethod: database.ProvisionerStorageMethodFile,
StorageSource: file.Hash,
Type: database.ProvisionerJobTypeTemplateVersionImport,
Input: []byte{'{', '}'},
})
if err != nil {
return xerrors.Errorf("insert provisioner job: %w", err)
}

// Create template version
templateVersion, err := s.InsertTemplateVersion(ctx, database.InsertTemplateVersionParams{
ID: uuid.New(),
TemplateID: uuid.NullUUID{
UUID: uuid.Nil,
Valid: false,
},
OrganizationID: opts.orgID,
CreatedAt: now,
UpdatedAt: now,
Name: namesgenerator.GetRandomName(1),
Readme: "",
JobID: job.ID,
CreatedBy: uuid.NullUUID{
UUID: opts.userID,
Valid: true,
},
})
if err != nil {
return xerrors.Errorf("insert template version: %w", err)
}

// Create template
template, err = s.InsertTemplate(ctx, database.InsertTemplateParams{
ID: uuid.New(),
CreatedAt: now,
UpdatedAt: now,
OrganizationID: opts.orgID,
Name: opts.name,
Provisioner: job.Provisioner,
ActiveVersionID: templateVersion.ID,
Description: "This template was auto-imported by Coder.",
MaxTtl: int64(maxTTLDefault),
MinAutostartInterval: int64(minAutostartIntervalDefault),
CreatedBy: opts.userID,
})
if err != nil {
return xerrors.Errorf("insert template: %w", err)
}

// Update template version with template ID
err = s.UpdateTemplateVersionByID(ctx, database.UpdateTemplateVersionByIDParams{
ID: templateVersion.ID,
TemplateID: uuid.NullUUID{
UUID: template.ID,
Valid: true,
},
})
if err != nil {
return xerrors.Errorf("update template version to set template ID: %s", err)
}

// Insert parameters at the template scope
for key, value := range opts.params {
_, err = s.InsertParameterValue(ctx, database.InsertParameterValueParams{
ID: uuid.New(),
Name: key,
CreatedAt: now,
UpdatedAt: now,
Scope: database.ParameterScopeTemplate,
ScopeID: template.ID,
SourceScheme: database.ParameterSourceSchemeData,
SourceValue: value,
DestinationScheme: database.ParameterDestinationSchemeProvisionerVariable,
})
if err != nil {
return xerrors.Errorf("insert template-scoped parameter %q with value %q: %w", key, value, err)
}
}

return nil
})

return template, err
}

func getCreatedByNamesByTemplateIDs(ctx context.Context, db database.Store, templates []database.Template) (map[string]string, error) {
creators := make(map[string]string, len(templates))
for _, template := range templates {
Expand Down
61 changes: 61 additions & 0 deletions coderd/users.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package coderd

import (
"bytes"
"context"
"crypto/sha256"
"database/sql"
Expand All @@ -9,6 +10,7 @@ import (
"net"
"net/http"
"net/url"
"os"
"strings"
"time"

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

"cdr.dev/slog"

"github.com/coder/coder/coderd/database"
"github.com/coder/coder/coderd/gitsshkey"
"github.com/coder/coder/coderd/httpapi"
Expand All @@ -27,6 +31,7 @@ import (
"github.com/coder/coder/coderd/userpassword"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/cryptorand"
"github.com/coder/coder/examples"
)

// Returns whether the initial user has been created or not.
Expand Down Expand Up @@ -82,6 +87,8 @@ func (api *API) postFirstUser(rw http.ResponseWriter, r *http.Request) {
Email: createUser.Email,
Username: createUser.Username,
Password: createUser.Password,
// Create an org for the first user.
OrganizationID: uuid.Nil,
},
LoginType: database.LoginTypePassword,
})
Expand Down Expand Up @@ -116,6 +123,60 @@ func (api *API) postFirstUser(rw http.ResponseWriter, r *http.Request) {
return
}

// Auto-import any designated templates into the new organization.
for _, template := range api.AutoImportTemplates {
archive, err := examples.Archive(string(template))
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error importing template.",
Detail: xerrors.Errorf("load template archive for %q: %w", template, err).Error(),
})
return
}

// Determine which parameter values to use.
parameters := map[string]string{}
switch template {
case AutoImportTemplateKubernetes:

// Determine the current namespace we're in.
const namespaceFile = "/var/run/secrets/kubernetes.io/serviceaccount/namespace"
namespace, err := os.ReadFile(namespaceFile)
if err != nil {
parameters["use_kubeconfig"] = "true" // use ~/.config/kubeconfig
parameters["namespace"] = "coder-workspaces"
} else {
parameters["use_kubeconfig"] = "false" // use SA auth
parameters["namespace"] = string(bytes.TrimSpace(namespace))
}

default:
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error importing template.",
Detail: fmt.Sprintf("cannot auto-import %q template", template),
})
return
}

tpl, err := api.autoImportTemplate(r.Context(), autoImportTemplateOpts{
name: string(template),
archive: archive,
params: parameters,
userID: user.ID,
orgID: organizationID,
})
if err != nil {
api.Logger.Warn(r.Context(), "failed to auto-import template", slog.F("template", template), slog.F("parameters", parameters), slog.Error(err))
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error importing template.",
Detail: xerrors.Errorf("failed to import template %q: %w", template, err).Error(),
})
return
}

api.Logger.Info(r.Context(), "auto-imported template", slog.F("id", tpl.ID), slog.F("template", template), slog.F("parameters", parameters))
}

httpapi.Write(rw, http.StatusCreated, codersdk.CreateFirstUserResponse{
UserID: user.ID,
OrganizationID: organizationID,
Expand Down
Loading