Auto import kubernetes template in Helm charts (#3550)

This commit is contained in:
Dean Sheather
2022-08-26 05:32:35 +10:00
committed by GitHub
parent 94e96fa40b
commit 14a9576b77
15 changed files with 423 additions and 41 deletions
+1
View File
@@ -66,6 +66,7 @@ type Options struct {
Telemetry telemetry.Reporter
TURNServer *turnconn.Server
TracerProvider *sdktrace.TracerProvider
AutoImportTemplates []AutoImportTemplate
LicenseHandler http.Handler
}
+2
View File
@@ -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
@@ -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()
+151
View File
@@ -2,7 +2,9 @@ package coderd
import (
"context"
"crypto/sha256"
"database/sql"
"encoding/hex"
"errors"
"fmt"
"net/http"
@@ -10,6 +12,7 @@ import (
"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"
@@ -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)
@@ -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 {
+61
View File
@@ -1,6 +1,7 @@
package coderd
import (
"bytes"
"context"
"crypto/sha256"
"database/sql"
@@ -9,6 +10,7 @@ import (
"net"
"net/http"
"net/url"
"os"
"strings"
"time"
@@ -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"
@@ -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.
@@ -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,
})
@@ -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,
+72
View File
@@ -13,6 +13,7 @@ import (
"github.com/stretchr/testify/require"
"golang.org/x/sync/errgroup"
"github.com/coder/coder/coderd"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/coderd/rbac"
"github.com/coder/coder/codersdk"
@@ -56,6 +57,77 @@ func TestFirstUser(t *testing.T) {
client := coderdtest.New(t, nil)
_ = coderdtest.CreateFirstUser(t, client)
})
t.Run("AutoImportsTemplates", func(t *testing.T) {
t.Parallel()
// All available auto import templates should be added to this list, and
// also added to the switch statement below.
autoImportTemplates := []coderd.AutoImportTemplate{
coderd.AutoImportTemplateKubernetes,
}
client := coderdtest.New(t, &coderdtest.Options{
AutoImportTemplates: autoImportTemplates,
})
u := coderdtest.CreateFirstUser(t, client)
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
templates, err := client.TemplatesByOrganization(ctx, u.OrganizationID)
require.NoError(t, err, "list templates")
require.Len(t, templates, len(autoImportTemplates), "listed templates count does not match")
require.ElementsMatch(t, autoImportTemplates, []coderd.AutoImportTemplate{
coderd.AutoImportTemplate(templates[0].Name),
}, "template names don't match")
for _, template := range templates {
// Check template parameters.
templateParams, err := client.Parameters(ctx, codersdk.ParameterTemplate, template.ID)
require.NoErrorf(t, err, "get template parameters for %q", template.Name)
// Ensure all template parameters are present.
expectedParams := map[string]bool{}
switch template.Name {
case "kubernetes":
expectedParams["use_kubeconfig"] = false
expectedParams["namespace"] = false
default:
t.Fatalf("unexpected template name %q", template.Name)
}
for _, v := range templateParams {
if _, ok := expectedParams[v.Name]; !ok {
t.Fatalf("unexpected template parameter %q in template %q", v.Name, template.Name)
}
expectedParams[v.Name] = true
}
for k, v := range expectedParams {
if !v {
t.Fatalf("missing template parameter %q in template %q", k, template.Name)
}
}
// Ensure template version is legit
templateVersion, err := client.TemplateVersion(ctx, template.ActiveVersionID)
require.NoErrorf(t, err, "get template version for %q", template.Name)
// Compare job parameters to template parameters.
jobParams, err := client.Parameters(ctx, codersdk.ParameterImportJob, templateVersion.Job.ID)
require.NoErrorf(t, err, "get template import job parameters for %q", template.Name)
for _, v := range jobParams {
if _, ok := expectedParams[v.Name]; !ok {
t.Fatalf("unexpected job parameter %q for template %q", v.Name, template.Name)
}
// Change it back to false so we can reuse the map
expectedParams[v.Name] = false
}
for k, v := range expectedParams {
if v {
t.Fatalf("missing job parameter %q for template %q", k, template.Name)
}
}
}
})
}
func TestPostLogin(t *testing.T) {