diff --git a/.gitignore b/.gitignore index 3e9cd9493bd89..d3deb0c72f550 100644 --- a/.gitignore +++ b/.gitignore @@ -34,6 +34,7 @@ dist/ site/out/ *.tfstate +*.tfstate.backup *.tfplan *.lock.hcl .terraform/ diff --git a/cli/server.go b/cli/server.go index f3f10760660ea..0a1d1ca364b83 100644 --- a/cli/server.go +++ b/cli/server.go @@ -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 ) @@ -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, @@ -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 != "" { @@ -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") diff --git a/coderd/coderd.go b/coderd/coderd.go index 010e48470d07d..e33cf5a69155d 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -66,6 +66,7 @@ type Options struct { Telemetry telemetry.Reporter TURNServer *turnconn.Server TracerProvider *sdktrace.TracerProvider + AutoImportTemplates []AutoImportTemplate LicenseHandler http.Handler } diff --git a/coderd/coderdtest/coderdtest.go b/coderd/coderdtest/coderdtest.go index d5503e21fe0aa..f7c7c04288ea6 100644 --- a/coderd/coderdtest/coderdtest.go +++ b/coderd/coderdtest/coderdtest.go @@ -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() diff --git a/coderd/templates.go b/coderd/templates.go index 065d9a6d463c2..eee18e0be1c14 100644 --- a/coderd/templates.go +++ b/coderd/templates.go @@ -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 { diff --git a/coderd/users.go b/coderd/users.go index 2429bc5a0a4a9..08175455b312e 100644 --- a/coderd/users.go +++ b/coderd/users.go @@ -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, diff --git a/coderd/users_test.go b/coderd/users_test.go index ddc9388db4064..e2752987f6951 100644 --- a/coderd/users_test.go +++ b/coderd/users_test.go @@ -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) { diff --git a/docs/install.md b/docs/install.md index f66a51e9d7cd6..1bc5d9181dc27 100644 --- a/docs/install.md +++ b/docs/install.md @@ -195,6 +195,12 @@ You will also need to have a Kubernetes cluster running K8s 1.19+. name: coder-db-url key: url + # This env variable controls whether or not to auto-import the + # "kubernetes" template on first startup. This will not work unless + # coder.serviceAccount.workspacePerms is true. + - name: CODER_TEMPLATE_AUTOIMPORT + value: "kubernetes" + tls: secretName: my-tls-secret-name ``` diff --git a/enterprise/coderd/licenses.go b/enterprise/coderd/licenses.go index 420d1c034b7cf..cad38eed91a46 100644 --- a/enterprise/coderd/licenses.go +++ b/enterprise/coderd/licenses.go @@ -37,6 +37,7 @@ var ValidMethods = []string{"EdDSA"} // key20220812 is the Coder license public key with id 2022-08-12 used to validate licenses signed // by our signing infrastructure +// //go:embed keys/2022-08-12 var key20220812 []byte @@ -134,12 +135,12 @@ func (a *licenseAPI) handler() http.Handler { // postLicense adds a new Enterprise license to the cluster. We allow multiple different licenses // in the cluster at one time for several reasons: // -// 1. Upgrades --- if the license format changes from one version of Coder to the next, during a -// rolling update you will have different Coder servers that need different licenses to function. -// 2. Avoid abrupt feature breakage --- when an admin uploads a new license with different features -// we generally don't want the old features to immediately break without warning. With a grace -// period on the license, features will continue to work from the old license until its grace -// period, then the users will get a warning allowing them to gracefully stop using the feature. +// 1. Upgrades --- if the license format changes from one version of Coder to the next, during a +// rolling update you will have different Coder servers that need different licenses to function. +// 2. Avoid abrupt feature breakage --- when an admin uploads a new license with different features +// we generally don't want the old features to immediately break without warning. With a grace +// period on the license, features will continue to work from the old license until its grace +// period, then the users will get a warning allowing them to gracefully stop using the feature. func (a *licenseAPI) postLicense(rw http.ResponseWriter, r *http.Request) { if !a.auth.Authorize(r, rbac.ActionCreate, rbac.ResourceLicense) { httpapi.Forbidden(rw) diff --git a/examples/templates/kubernetes-pod/README.md b/examples/templates/kubernetes/README.md similarity index 86% rename from examples/templates/kubernetes-pod/README.md rename to examples/templates/kubernetes/README.md index fa4569846b79f..6e8bbf6e410f8 100644 --- a/examples/templates/kubernetes-pod/README.md +++ b/examples/templates/kubernetes/README.md @@ -1,14 +1,16 @@ --- -name: Develop multiple services in Kubernetes +name: Develop in Kubernetes description: Get started with Kubernetes development. tags: [cloud, kubernetes] --- # Getting started +This template creates a pod running the `codercom/enterprise-base:ubuntu` image. + ## RBAC -The Coder provisioner requires permission to administer pods to use this template. The template +The Coder provisioner requires permission to administer pods to use this template. The template creates workspaces in a single Kubernetes namespace, using the `workspaces_namespace` parameter set while creating the template. @@ -20,15 +22,15 @@ kind: Role metadata: name: coder rules: -- apiGroups: [""] - resources: ["pods"] - verbs: ["*"] + - apiGroups: [""] + resources: ["pods"] + verbs: ["*"] ``` ## Authentication This template can authenticate using in-cluster authentication, or using a kubeconfig local to the -Coder host. For additional authentication options, consult the [Kubernetes provider +Coder host. For additional authentication options, consult the [Kubernetes provider documentation](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs). ### kubeconfig on Coder host @@ -46,8 +48,8 @@ you can use in-cluster authentication. To use this authentication, set the parameter `use_kubeconfig` to false. The Terraform provisioner will automatically use the service account associated with the pod to -authenticate to Kubernetes. Be sure to bind a [role with appropriate permission](#rbac) to the -service account. For example, assuming the Coder host runs in the same namespace as you intend +authenticate to Kubernetes. Be sure to bind a [role with appropriate permission](#rbac) to the +service account. For example, assuming the Coder host runs in the same namespace as you intend to create workspaces: ```yaml diff --git a/examples/templates/kubernetes-pod/main.tf b/examples/templates/kubernetes/main.tf similarity index 75% rename from examples/templates/kubernetes-pod/main.tf rename to examples/templates/kubernetes/main.tf index 2a28e607750c0..67c9cfcc4882f 100644 --- a/examples/templates/kubernetes-pod/main.tf +++ b/examples/templates/kubernetes/main.tf @@ -25,17 +25,21 @@ variable "use_kubeconfig" { EOF } -variable "coder_namespace" { +variable "namespace" { type = string sensitive = true description = "The namespace to create workspaces in (must exist prior to creating workspaces)" - default = "coder-namespace" + default = "coder-workspaces" } -variable "disk_size" { - type = number - description = "Disk size (__ GB)" +variable "home_disk_size" { + type = number + description = "How large would you like your home volume to be (in GB)?" default = 10 + validation { + condition = var.home_disk_size >= 1 + error_message = "Value must be greater than or equal to 1." + } } provider "kubernetes" { @@ -46,8 +50,8 @@ provider "kubernetes" { data "coder_workspace" "me" {} resource "coder_agent" "main" { - os = "linux" - arch = "amd64" + os = "linux" + arch = "amd64" startup_script = < 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