From ce2716d5522d07869dfd54fb6af4e8e90be5d090 Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Thu, 18 Aug 2022 12:49:48 +0000 Subject: [PATCH 1/6] Add basic kubernetes example template --- .gitignore | 1 + examples/templates/kubernetes/README.md | 80 ++++++++++++++++++ examples/templates/kubernetes/main.tf | 105 ++++++++++++++++++++++++ 3 files changed, 186 insertions(+) create mode 100644 examples/templates/kubernetes/README.md create mode 100644 examples/templates/kubernetes/main.tf 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/examples/templates/kubernetes/README.md b/examples/templates/kubernetes/README.md new file mode 100644 index 0000000000000..f58363fb86c96 --- /dev/null +++ b/examples/templates/kubernetes/README.md @@ -0,0 +1,80 @@ +--- +name: Develop in Kubernetes +description: Get started with Kubernetes development. +tags: [cloud, kubernetes] +--- + +# Getting started + +This template provides a stripped down version of the `kubernetes-multi-service` +example template with only one agent/container instead of three. + +## RBAC + +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. + +Create a role as follows and bind it to the user or service account that runs the coder host. + +```yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: coder +rules: + - apiGroups: [""] + resources: ["pods"] + verbs: ["*"] + - apiGroups: [""] + resources: ["persistentvolumeclaims"] + 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 +documentation](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs). + +### kubeconfig on Coder host + +If the Coder host has a local `~/.kube/config`, you can use this to authenticate +with Coder. Make sure this is done with same user that's running the `coder` service. + +To use this authentication, set the parameter `use_kubeconfig` to true. + +### In-cluster authentication + +If the Coder host runs in a Pod on the same Kubernetes cluster as you are creating workspaces in, +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 +to create workspaces: + +```yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: coder + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: coder +subjects: + - kind: ServiceAccount + name: coder +roleRef: + kind: Role + name: coder + apiGroup: rbac.authorization.k8s.io +``` + +Then start the Coder host with `serviceAccountName: coder` in the pod spec. + diff --git a/examples/templates/kubernetes/main.tf b/examples/templates/kubernetes/main.tf new file mode 100644 index 0000000000000..0a5ab4b0c3dc1 --- /dev/null +++ b/examples/templates/kubernetes/main.tf @@ -0,0 +1,105 @@ +terraform { + required_providers { + coder = { + source = "coder/coder" + version = "~> 0.4.3" + } + kubernetes = { + source = "hashicorp/kubernetes" + version = "~> 2.10" + } + } +} + +variable "use_kubeconfig" { + type = bool + sensitive = true + description = <<-EOF + Use host kubeconfig? (true/false) + + Set this to false if the Coder host is itself running as a Pod on the same + Kubernetes cluster as you are deploying workspaces to. + + Set this to true if the Coder host is running outside the Kubernetes cluster + for workspaces. A valid "~/.kube/config" must be present on the Coder host. + EOF +} + +variable "workspaces_namespace" { + type = string + sensitive = true + description = "The namespace to create workspaces in (must exist prior to creating workspaces)" + default = "coder-workspaces" +} + +variable "home_size" { + type = number + description = "How large would you like your home volume to be (in GB)?" + default = 10 + validation { + condition = var.home_size >= 1 + error_message = "Value must be greater than or equal to 1." + } +} + +provider "kubernetes" { + # Authenticate via ~/.kube/config or a Coder-specific ServiceAccount, depending on admin preferences + config_path = var.use_kubeconfig == true ? "~/.kube/config" : null +} + +data "coder_workspace" "me" {} + +resource "coder_agent" "main" { + os = "linux" + arch = "amd64" +} + +resource "kubernetes_persistent_volume_claim" "home" { + metadata { + name = "coder-${data.coder_workspace.me.owner}-${data.coder_workspace.me.name}-home" + namespace = var.workspaces_namespace + } + spec { + access_modes = ["ReadWriteOnce"] + resources { + requests = { + storage = "${var.home_size}Gi" + } + } + } +} + +resource "kubernetes_pod" "main" { + count = data.coder_workspace.me.start_count + metadata { + name = "coder-${data.coder_workspace.me.owner}-${data.coder_workspace.me.name}" + namespace = var.workspaces_namespace + } + spec { + container { + name = data.coder_workspace.me.name + image = "mcr.microsoft.com/vscode/devcontainers/base:ubuntu" + command = ["sh", "-c", coder_agent.main.init_script] + security_context { + run_as_user = "1000" + } + env { + name = "CODER_AGENT_TOKEN" + value = coder_agent.main.token + } + volume_mount { + name = "home" + read_only = false + mount_path = "/home/vscode" + } + } + + volume { + name = "home" + persistent_volume_claim { + claim_name = kubernetes_persistent_volume_claim.home.metadata[0].name + read_only = false + } + } + } +} From 514a11932977c144a47c19a1cdacc171fcd015c4 Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Thu, 18 Aug 2022 12:50:21 +0000 Subject: [PATCH 2/6] Add service account to helm chart --- .../templates/{deployment.yaml => coder.yaml} | 8 ++++++ helm/templates/rbac.yaml | 27 +++++++++++++++++++ helm/values.yaml | 27 ++++++++++++++++--- 3 files changed, 59 insertions(+), 3 deletions(-) rename helm/templates/{deployment.yaml => coder.yaml} (95%) create mode 100644 helm/templates/rbac.yaml diff --git a/helm/templates/deployment.yaml b/helm/templates/coder.yaml similarity index 95% rename from helm/templates/deployment.yaml rename to helm/templates/coder.yaml index cc4a66839e3ad..9bf9cbfe9a804 100644 --- a/helm/templates/deployment.yaml +++ b/helm/templates/coder.yaml @@ -1,3 +1,10 @@ +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: coder + +--- apiVersion: apps/v1 kind: Deployment metadata: @@ -12,6 +19,7 @@ spec: selector: matchLabels: {{- include "coder.selectorLabels" . | nindent 6 }} + serviceAccountName: coder template: metadata: labels: diff --git a/helm/templates/rbac.yaml b/helm/templates/rbac.yaml new file mode 100644 index 0000000000000..ede94b4e76f2e --- /dev/null +++ b/helm/templates/rbac.yaml @@ -0,0 +1,27 @@ +{{- if .Values.coder.serviceAccount.workspacePerms }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: coder-workspace-perms +rules: + - apiGroups: [""] + resources: ["pods"] + verbs: ["*"] + - apiGroups: [""] + resources: ["persistentvolumeclaims"] + verbs: ["*"] + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: coder +subjects: + - kind: ServiceAccount + name: coder +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: coder-workspace-perms +{{- end }} diff --git a/helm/values.yaml b/helm/values.yaml index 2090296dc467d..b22f33cf2e545 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -16,6 +16,18 @@ coder: # https://kubernetes.io/docs/concepts/containers/images/#image-pull-policy pullPolicy: IfNotPresent + # coder.serviceAccount -- Configuration for the automatically created service + # account. Creation of the service account cannot be disabled. + serviceAccount: + # coder.serviceAccount.workspacePerms -- Whether or not to grant the coder + # service account permissions to manage workspaces. This includes + # permission to manage pods and persistent volume claims in the deployment + # namespace. + # + # It is recommended to keep this on if you are using Kubernetes templates + # within Coder. + workspacePerms: true + # coder.env -- The environment variables to set for Coder. These can be used # to configure all aspects of `coder server`. Please see `coder server --help` # for information about what environment variables can be set. @@ -27,10 +39,18 @@ coder: # - CODER_TLS_CERT_FILE: set if tls.secretName is not empty. # - CODER_TLS_KEY_FILE: set if tls.secretName is not empty. env: + # You'll likely want to set these variables to something other than the + # defaults. - name: CODER_ACCESS_URL value: "https://coder.example.com" - #- name: CODER_PG_CONNECTION_URL - # value: "postgres://coder:password@postgres:5432/coder?sslmode=disable" + - name: CODER_PG_CONNECTION_URL + value: "postgres://coder:password@postgres:5432/coder?sslmode=disable" + + # 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" # coder.tls -- The TLS configuration for Coder. tls: @@ -43,7 +63,8 @@ coder: # coder.resources -- The resources to request for Coder. These are optional # and are not set by default. - resources: {} + resources: + {} # limits: # cpu: 100m # memory: 128Mi From 1ada595e10a9eb8fb633b88006802566c81d71da Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Thu, 18 Aug 2022 14:49:34 +0000 Subject: [PATCH 3/6] Add template auto importing to coderd --- cli/server.go | 37 ++++++-- coderd/coderd.go | 1 + coderd/coderdtest/coderdtest.go | 2 + coderd/templates.go | 152 ++++++++++++++++++++++++++++++++ coderd/users.go | 61 +++++++++++++ coderd/users_test.go | 74 ++++++++++++++++ 6 files changed, 322 insertions(+), 5 deletions(-) diff --git a/cli/server.go b/cli/server.go index 86016d9c9bd7a..15901b1a46472 100644 --- a/cli/server.go +++ b/cli/server.go @@ -108,6 +108,7 @@ func server() *cobra.Command { trace bool secureAuthCookie bool sshKeygenAlgorithmRaw string + autoImportTemplates []string spooky bool verbose bool ) @@ -271,6 +272,30 @@ func server() *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 + case "kubernetes-multi-service": + v = coderd.AutoImportTemplateKubernetesMultiService + 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, @@ -284,6 +309,7 @@ func server() *cobra.Command { TURNServer: turnServer, TracerProvider: tracerProvider, Telemetry: telemetry.NewNoop(), + AutoImportTemplates: validatedAutoImportTemplates, } if oauth2GithubClientSecret != "" { @@ -739,6 +765,7 @@ func server() *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, kubernetes-multi-service") 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") @@ -881,16 +908,16 @@ func newProvisionerDaemon(ctx context.Context, coderAPI *coderd.API, // nolint: revive func printLogo(cmd *cobra.Command, spooky bool) { if spooky { - _, _ = fmt.Fprintf(cmd.OutOrStdout(), `▄████▄ ▒█████ ▓█████▄ ▓█████ ██▀███ + _, _ = fmt.Fprintf(cmd.OutOrStdout(), `▄████▄ ▒█████ ▓█████▄ ▓█████ ██▀███ ▒██▀ ▀█ ▒██▒ ██▒▒██▀ ██▌▓█ ▀ ▓██ ▒ ██▒ ▒▓█ ▄ ▒██░ ██▒░██ █▌▒███ ▓██ ░▄█ ▒ -▒▓▓▄ ▄██▒▒██ ██░░▓█▄ ▌▒▓█ ▄ ▒██▀▀█▄ +▒▓▓▄ ▄██▒▒██ ██░░▓█▄ ▌▒▓█ ▄ ▒██▀▀█▄ ▒ ▓███▀ ░░ ████▓▒░░▒████▓ ░▒████▒░██▓ ▒██▒ ░ ░▒ ▒ ░░ ▒░▒░▒░ ▒▒▓ ▒ ░░ ▒░ ░░ ▒▓ ░▒▓░ ░ ▒ ░ ▒ ▒░ ░ ▒ ▒ ░ ░ ░ ░▒ ░ ▒░ -░ ░ ░ ░ ▒ ░ ░ ░ ░ ░░ ░ -░ ░ ░ ░ ░ ░ ░ ░ -░ ░ +░ ░ ░ ░ ▒ ░ ░ ░ ░ ░░ ░ +░ ░ ░ ░ ░ ░ ░ ░ +░ ░ `) return } diff --git a/coderd/coderd.go b/coderd/coderd.go index cf29aa986fc22..59b5c5c244061 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 } // New constructs a Coder API handler. diff --git a/coderd/coderdtest/coderdtest.go b/coderd/coderdtest/coderdtest.go index 90f9482999862..394ebb4ac99f7 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 @@ -198,6 +199,7 @@ func newWithCloser(t *testing.T, options *Options) (*codersdk.Client, io.Closer) 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 321b9872a5f46..682100874ea4b 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,15 @@ 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" + AutoImportTemplateKubernetesMultiService AutoImportTemplate = "kubernetes-multi-service" +) + // Returns a single template. func (api *API) template(rw http.ResponseWriter, r *http.Request) { template := httpmw.TemplateParam(r) @@ -477,6 +489,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) + } + } + + // 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) + } + } + + 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 e77364eeba6ef..5acc2c06525e8 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, AutoImportTemplateKubernetesMultiService: + + // 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["workspaces_namespace"] = "coder-workspaces" + } else { + parameters["use_kubeconfig"] = "false" // use SA auth + parameters["workspaces_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 bf372f01f182d..ac6840bc993b9 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,79 @@ 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, + coderd.AutoImportTemplateKubernetesMultiService, + } + 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, 2, "should have two templates") + require.ElementsMatch(t, autoImportTemplates, []coderd.AutoImportTemplate{ + coderd.AutoImportTemplate(templates[0].Name), + coderd.AutoImportTemplate(templates[1].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", "kubernetes-multi-service": + expectedParams["use_kubeconfig"] = false + expectedParams["workspaces_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) { From e576ca0cc47ecaa4a6af477272f5ee3c9874c7d5 Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Thu, 18 Aug 2022 16:34:09 +0000 Subject: [PATCH 4/6] fixup! Add template auto importing to coderd --- coderd/templates.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/coderd/templates.go b/coderd/templates.go index 682100874ea4b..827d1b217bb7c 100644 --- a/coderd/templates.go +++ b/coderd/templates.go @@ -532,7 +532,7 @@ func (api *API) autoImportTemplate(ctx context.Context, opts autoImportTemplateO DestinationScheme: database.ParameterDestinationSchemeProvisionerVariable, }) if err != nil { - return xerrors.Errorf("insert job-scoped parameter %q with value %q: %w", key, value) + return xerrors.Errorf("insert job-scoped parameter %q with value %q: %w", key, value, err) } } @@ -619,7 +619,7 @@ func (api *API) autoImportTemplate(ctx context.Context, opts autoImportTemplateO DestinationScheme: database.ParameterDestinationSchemeProvisionerVariable, }) if err != nil { - return xerrors.Errorf("insert template-scoped parameter %q with value %q: %w", key, value) + return xerrors.Errorf("insert template-scoped parameter %q with value %q: %w", key, value, err) } } From 345fc7e8f3e3b12f297f1b94f90bf3929429ea13 Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Thu, 25 Aug 2022 19:02:25 +0000 Subject: [PATCH 5/6] fix: move serviceAccountName to correct spot --- helm/templates/coder.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/templates/coder.yaml b/helm/templates/coder.yaml index 9bf9cbfe9a804..d89709775f0b4 100644 --- a/helm/templates/coder.yaml +++ b/helm/templates/coder.yaml @@ -19,12 +19,12 @@ spec: selector: matchLabels: {{- include "coder.selectorLabels" . | nindent 6 }} - serviceAccountName: coder template: metadata: labels: {{- include "coder.selectorLabels" . | nindent 8 }} spec: + serviceAccountName: coder restartPolicy: Always terminationGracePeriodSeconds: 60 containers: From 89c3f9966f0cf4809a68f8b2c4532bbc231589c5 Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Thu, 25 Aug 2022 19:20:57 +0000 Subject: [PATCH 6/6] fix template problems --- cli/server.go | 4 +- coderd/templates.go | 3 +- coderd/users.go | 6 +- coderd/users_test.go | 8 +- enterprise/coderd/licenses.go | 13 ++- examples/templates/kubernetes-pod/README.md | 111 ------------------ examples/templates/kubernetes-pod/main.tf | 118 -------------------- examples/templates/kubernetes/README.md | 51 +++++++-- examples/templates/kubernetes/main.tf | 48 +++++--- 9 files changed, 91 insertions(+), 271 deletions(-) delete mode 100644 examples/templates/kubernetes-pod/README.md delete mode 100644 examples/templates/kubernetes-pod/main.tf diff --git a/cli/server.go b/cli/server.go index 6c14ef9817265..0a1d1ca364b83 100644 --- a/cli/server.go +++ b/cli/server.go @@ -296,8 +296,6 @@ func Server(newAPI func(*coderd.Options) *coderd.API) *cobra.Command { switch autoImportTemplate { case "kubernetes": v = coderd.AutoImportTemplateKubernetes - case "kubernetes-multi-service": - v = coderd.AutoImportTemplateKubernetesMultiService default: return xerrors.Errorf("auto import template %q is not supported", autoImportTemplate) } @@ -770,7 +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, kubernetes-multi-service") + 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/templates.go b/coderd/templates.go index 9d73fdfc52e43..eee18e0be1c14 100644 --- a/coderd/templates.go +++ b/coderd/templates.go @@ -34,8 +34,7 @@ var ( type AutoImportTemplate string const ( - AutoImportTemplateKubernetes AutoImportTemplate = "kubernetes" - AutoImportTemplateKubernetesMultiService AutoImportTemplate = "kubernetes-multi-service" + AutoImportTemplateKubernetes AutoImportTemplate = "kubernetes" ) // Returns a single template. diff --git a/coderd/users.go b/coderd/users.go index fe546bb15be13..08175455b312e 100644 --- a/coderd/users.go +++ b/coderd/users.go @@ -137,17 +137,17 @@ func (api *API) postFirstUser(rw http.ResponseWriter, r *http.Request) { // Determine which parameter values to use. parameters := map[string]string{} switch template { - case AutoImportTemplateKubernetes, AutoImportTemplateKubernetesMultiService: + 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["workspaces_namespace"] = "coder-workspaces" + parameters["namespace"] = "coder-workspaces" } else { parameters["use_kubeconfig"] = "false" // use SA auth - parameters["workspaces_namespace"] = string(bytes.TrimSpace(namespace)) + parameters["namespace"] = string(bytes.TrimSpace(namespace)) } default: diff --git a/coderd/users_test.go b/coderd/users_test.go index ba34e8151ae51..e2752987f6951 100644 --- a/coderd/users_test.go +++ b/coderd/users_test.go @@ -65,7 +65,6 @@ func TestFirstUser(t *testing.T) { // also added to the switch statement below. autoImportTemplates := []coderd.AutoImportTemplate{ coderd.AutoImportTemplateKubernetes, - coderd.AutoImportTemplateKubernetesMultiService, } client := coderdtest.New(t, &coderdtest.Options{ AutoImportTemplates: autoImportTemplates, @@ -77,10 +76,9 @@ func TestFirstUser(t *testing.T) { templates, err := client.TemplatesByOrganization(ctx, u.OrganizationID) require.NoError(t, err, "list templates") - require.Len(t, templates, 2, "should have two templates") + require.Len(t, templates, len(autoImportTemplates), "listed templates count does not match") require.ElementsMatch(t, autoImportTemplates, []coderd.AutoImportTemplate{ coderd.AutoImportTemplate(templates[0].Name), - coderd.AutoImportTemplate(templates[1].Name), }, "template names don't match") for _, template := range templates { @@ -91,9 +89,9 @@ func TestFirstUser(t *testing.T) { // Ensure all template parameters are present. expectedParams := map[string]bool{} switch template.Name { - case "kubernetes", "kubernetes-multi-service": + case "kubernetes": expectedParams["use_kubeconfig"] = false - expectedParams["workspaces_namespace"] = false + expectedParams["namespace"] = false default: t.Fatalf("unexpected template name %q", template.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-pod/README.md deleted file mode 100644 index fa4569846b79f..0000000000000 --- a/examples/templates/kubernetes-pod/README.md +++ /dev/null @@ -1,111 +0,0 @@ ---- -name: Develop multiple services in Kubernetes -description: Get started with Kubernetes development. -tags: [cloud, kubernetes] ---- - -# Getting started - -## RBAC - -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. - -Create a role as follows and bind it to the user or service account that runs the coder host. - -```yaml -apiVersion: rbac.authorization.k8s.io/v1 -kind: Role -metadata: - name: coder -rules: -- 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 -documentation](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs). - -### kubeconfig on Coder host - -If the Coder host has a local `~/.kube/config`, you can use this to authenticate -with Coder. Make sure this is done with same user that's running the `coder` service. - -To use this authentication, set the parameter `use_kubeconfig` to true. - -### In-cluster authentication - -If the Coder host runs in a Pod on the same Kubernetes cluster as you are creating workspaces in, -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 -to create workspaces: - -```yaml -apiVersion: v1 -kind: ServiceAccount -metadata: - name: coder - ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: RoleBinding -metadata: - name: coder -subjects: - - kind: ServiceAccount - name: coder -roleRef: - kind: Role - name: coder - apiGroup: rbac.authorization.k8s.io -``` - -Then start the Coder host with `serviceAccountName: coder` in the pod spec. - -## Namespace - -The target namespace in which the pod will be deployed is defined via the `coder_workspace` -variable. The namespace must exist prior to creating workspaces. - -## Persistence - -The `/home/coder` directory in this example is persisted via the attached PersistentVolumeClaim. -Any data saved outside of this directory will be wiped when the workspace stops. - -Since most binary installations and environment configurations live outside of -the `/home` directory, we suggest including these in the `startup_script` argument -of the `coder_agent` resource block, which will run each time the workspace starts up. - -For example, when installing the `aws` CLI, the install script will place the -`aws` binary in `/usr/local/bin/aws`. To ensure the `aws` CLI is persisted across -workspace starts/stops, include the following code in the `coder_agent` resource -block of your workspace template: - -```terraform -resource "coder_agent" "main" { - startup_script = <= 1 + condition = var.home_disk_size >= 1 error_message = "Value must be greater than or equal to 1." } } @@ -50,20 +50,36 @@ provider "kubernetes" { data "coder_workspace" "me" {} resource "coder_agent" "main" { - os = "linux" - arch = "amd64" + os = "linux" + arch = "amd64" + startup_script = <