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 = < 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