diff --git a/pkg/router/cache_redis.go b/pkg/router/cache_redis.go index 8a18b94..cc7a81d 100644 --- a/pkg/router/cache_redis.go +++ b/pkg/router/cache_redis.go @@ -2,6 +2,7 @@ package router import ( "context" + "crypto/tls" "fmt" "github.com/go-redis/redis" @@ -12,10 +13,19 @@ type CacheRedis struct { redis *redis.Client } -func NewCacheRedis(addr string) (*CacheRedis, error) { +func NewCacheRedis(addr, password string, secure bool) (*CacheRedis, error) { fmt.Printf("ns=cache.redis at=new addr=%s\n", addr) - rc := redis.NewClient(&redis.Options{Addr: addr}) + opts := &redis.Options{ + Addr: addr, + Password: password, + } + + if secure { + opts.TLSConfig = &tls.Config{} + } + + rc := redis.NewClient(opts) if _, err := rc.Ping().Result(); err != nil { return nil, err diff --git a/pkg/router/router.go b/pkg/router/router.go index aa0ee67..d58b95f 100644 --- a/pkg/router/router.go +++ b/pkg/router/router.go @@ -75,7 +75,7 @@ func New() (*Router, error) { r.cache = c case "redis": - c, err := NewCacheRedis(os.Getenv("REDIS_ADDR")) + c, err := NewCacheRedis(os.Getenv("REDIS_ADDR"), os.Getenv("REDIS_AUTH"), os.Getenv("REDIS_SECURE") == "true") if err != nil { return nil, err } diff --git a/provider/do/app.go b/provider/do/app.go new file mode 100644 index 0000000..8c92379 --- /dev/null +++ b/provider/do/app.go @@ -0,0 +1,13 @@ +package do + +func (p *Provider) AppIdles(name string) (bool, error) { + return false, nil +} + +func (p *Provider) AppParameters() map[string]string { + return map[string]string{} +} + +func (p *Provider) AppStatus(name string) (string, error) { + return "running", nil +} diff --git a/provider/do/build.go b/provider/do/build.go new file mode 100644 index 0000000..fc6fc7b --- /dev/null +++ b/provider/do/build.go @@ -0,0 +1,75 @@ +package do + +import ( + "fmt" + "io" + "net/url" + "os/exec" + "strings" + + "github.com/convox/convox/pkg/structs" +) + +func (p *Provider) BuildExport(app, id string, w io.Writer) error { + if err := p.authAppRepository(app); err != nil { + return err + } + + return p.Provider.BuildExport(app, id, w) +} + +func (p *Provider) BuildImport(app string, r io.Reader) (*structs.Build, error) { + if err := p.authAppRepository(app); err != nil { + return nil, err + } + + return p.Provider.BuildImport(app, r) +} + +func (p *Provider) BuildLogs(app, id string, opts structs.LogsOptions) (io.ReadCloser, error) { + b, err := p.BuildGet(app, id) + if err != nil { + return nil, err + } + + opts.Since = nil + + switch b.Status { + case "running": + return p.ProcessLogs(app, b.Process, opts) + default: + u, err := url.Parse(b.Logs) + if err != nil { + return nil, err + } + + switch u.Scheme { + case "object": + return p.ObjectFetch(u.Hostname(), u.Path) + default: + return nil, fmt.Errorf("unable to read logs for build: %s", id) + } + } +} + +func (p *Provider) authAppRepository(app string) error { + repo, _, err := p.RepositoryHost(app) + if err != nil { + return err + } + + user, pass, err := p.RepositoryAuth(app) + if err != nil { + return err + } + + cmd := exec.Command("docker", "login", "-u", user, "--password-stdin", repo) + + cmd.Stdin = strings.NewReader(pass) + + if err := cmd.Run(); err != nil { + return err + } + + return nil +} diff --git a/provider/do/deployment.go b/provider/do/deployment.go new file mode 100644 index 0000000..535c8f3 --- /dev/null +++ b/provider/do/deployment.go @@ -0,0 +1,5 @@ +package do + +func (p *Provider) DeploymentTimeout() int { + return 1800 +} diff --git a/provider/do/do.go b/provider/do/do.go new file mode 100644 index 0000000..cb9c2f4 --- /dev/null +++ b/provider/do/do.go @@ -0,0 +1,87 @@ +package do + +import ( + "context" + "os" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/credentials" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/s3" + "github.com/aws/aws-sdk-go/service/s3/s3iface" + "github.com/convox/convox/pkg/structs" + "github.com/convox/convox/provider/k8s" + "k8s.io/apimachinery/pkg/util/runtime" +) + +type Provider struct { + *k8s.Provider + + Bucket string + Region string + Registry string + Secret string + SpacesAccess string + SpacesEndpoint string + SpacesSecret string + + S3 s3iface.S3API +} + +func FromEnv() (*Provider, error) { + k, err := k8s.FromEnv() + if err != nil { + return nil, err + } + + p := &Provider{ + Provider: k, + Bucket: os.Getenv("BUCKET"), + Region: os.Getenv("REGION"), + Registry: os.Getenv("REGISTRY"), + Secret: os.Getenv("SECRET"), + SpacesAccess: os.Getenv("SPACES_ACCESS"), + SpacesEndpoint: os.Getenv("SPACES_ENDPOINT"), + SpacesSecret: os.Getenv("SPACES_SECRET"), + } + + k.Engine = p + + return p, nil +} + +func (p *Provider) Initialize(opts structs.ProviderOptions) error { + if err := p.initializeDOServices(); err != nil { + return err + } + + if err := p.Provider.Initialize(opts); err != nil { + return err + } + + runtime.ErrorHandlers = []func(error){} + + return nil +} + +func (p *Provider) WithContext(ctx context.Context) structs.Provider { + pp := *p + pp.Provider = pp.Provider.WithContext(ctx).(*k8s.Provider) + return &pp +} + +func (p *Provider) initializeDOServices() error { + s, err := session.NewSession(&aws.Config{ + Region: aws.String(p.Region), + Credentials: credentials.NewStaticCredentials(p.SpacesAccess, p.SpacesSecret, ""), + }) + if err != nil { + return err + } + + p.S3 = s3.New(s, &aws.Config{ + Endpoint: aws.String(p.SpacesEndpoint), + }) + + return nil +} diff --git a/provider/do/heartbeat.go b/provider/do/heartbeat.go new file mode 100644 index 0000000..b535880 --- /dev/null +++ b/provider/do/heartbeat.go @@ -0,0 +1,10 @@ +package do + +func (p *Provider) Heartbeat() (map[string]interface{}, error) { + hs := map[string]interface{}{ + "instance_type": "unknown", + "region": p.Region, + } + + return hs, nil +} diff --git a/provider/do/helpers.go b/provider/do/helpers.go new file mode 100644 index 0000000..3b2d108 --- /dev/null +++ b/provider/do/helpers.go @@ -0,0 +1,106 @@ +package do + +import ( + "context" + "errors" + "fmt" + "os" + "os/exec" + "regexp" + "strings" + "time" + + // gv "github.com/GoogleCloudPlatform/gke-managed-certs/pkg/clientgen/clientset/versioned" + am "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func (p *Provider) appRegistry(app string) (string, error) { + ns, err := p.Provider.Cluster.CoreV1().Namespaces().Get(p.AppNamespace(app), am.GetOptions{}) + if err != nil { + return "", err + } + + registry, ok := ns.ObjectMeta.Annotations["convox.registry"] + if !ok { + return "", fmt.Errorf("no registry for app: %s", app) + } + + return registry, nil +} + +// func (p *Provider) gkeManagedCertsClient() (gv.Interface, error) { +// return gv.NewForConfig(p.Config) +// } + +func (p *Provider) watchForProcessTermination(ctx context.Context, app, pid string, cancel func()) { + defer cancel() + + tick := time.NewTicker(2 * time.Second) + defer tick.Stop() + + for { + select { + case <-ctx.Done(): + return + case <-tick.C: + if _, err := p.ProcessGet(app, pid); err != nil { + time.Sleep(2 * time.Second) + cancel() + return + } + } + } +} + +func kubectl(args ...string) error { + cmd := exec.Command("kubectl", args...) + + cmd.Env = os.Environ() + + out, err := cmd.CombinedOutput() + if err != nil { + return errors.New(strings.TrimSpace(string(out))) + } + + return nil +} + +var outputConverter = regexp.MustCompile("([a-z])([A-Z])") // lower case letter followed by upper case + +func outputToEnvironment(name string) string { + return strings.ToUpper(outputConverter.ReplaceAllString(name, "${1}_${2}")) +} + +func upperName(name string) string { + if name == "" { + return "" + } + + // replace underscores with dashes + name = strings.Replace(name, "_", "-", -1) + + // myapp -> Myapp; my-app -> MyApp + us := strings.ToUpper(name[0:1]) + name[1:] + + for { + i := strings.Index(us, "-") + + if i == -1 { + break + } + + s := us[0:i] + + if len(us) > i+1 { + s += strings.ToUpper(us[i+1 : i+2]) + } + + if len(us) > i+2 { + s += us[i+2:] + } + + us = s + } + + return us +} diff --git a/provider/do/ingress.go b/provider/do/ingress.go new file mode 100644 index 0000000..0ec86fc --- /dev/null +++ b/provider/do/ingress.go @@ -0,0 +1,13 @@ +package do + +func (p *Provider) IngressAnnotations(app string) (map[string]string, error) { + ans := map[string]string{ + "kubernetes.io/ingress.class": "convox", + } + + return ans, nil +} + +func (p *Provider) IngressSecrets(app string) ([]string, error) { + return []string{}, nil +} diff --git a/provider/do/log.go b/provider/do/log.go new file mode 100644 index 0000000..81ad5cb --- /dev/null +++ b/provider/do/log.go @@ -0,0 +1,21 @@ +package do + +import ( + "io" + "io/ioutil" + "strings" + "sync" + "time" + + "github.com/convox/convox/pkg/structs" +) + +var sequenceTokens sync.Map + +func (p *Provider) Log(app, stream string, ts time.Time, message string) error { + return nil +} + +func (p *Provider) AppLogs(name string, opts structs.LogsOptions) (io.ReadCloser, error) { + return ioutil.NopCloser(strings.NewReader("")), nil +} diff --git a/provider/do/manifest.go b/provider/do/manifest.go new file mode 100644 index 0000000..5361eaf --- /dev/null +++ b/provider/do/manifest.go @@ -0,0 +1,9 @@ +package do + +import ( + "github.com/convox/convox/pkg/manifest" +) + +func (p *Provider) ManifestValidate(m *manifest.Manifest) error { + return nil +} diff --git a/provider/do/object.go b/provider/do/object.go new file mode 100644 index 0000000..c5a7ec9 --- /dev/null +++ b/provider/do/object.go @@ -0,0 +1,163 @@ +package do + +import ( + "crypto/rand" + "crypto/sha256" + "encoding/hex" + "fmt" + "io" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/s3" + "github.com/aws/aws-sdk-go/service/s3/s3manager" + "github.com/convox/convox/pkg/structs" +) + +func (p *Provider) ObjectDelete(app, key string) error { + exists, err := p.ObjectExists(app, key) + if err != nil { + return err + } + + if !exists { + return fmt.Errorf("object not found: %s", key) + } + + _, err = p.S3.DeleteObject(&s3.DeleteObjectInput{ + Bucket: aws.String(p.Bucket), + Key: aws.String(p.objectKey(app, key)), + }) + if err != nil { + return err + } + + return nil +} + +func (p *Provider) ObjectExists(app, key string) (bool, error) { + _, err := p.S3.HeadObject(&s3.HeadObjectInput{ + Bucket: aws.String(p.Bucket), + Key: aws.String(p.objectKey(app, key)), + }) + if err, ok := err.(awserr.Error); ok && err.Code() == "NotFound" { + return false, nil + } + if err != nil { + return false, err + } + + return true, nil +} + +// ObjectFetch fetches an Object +func (p *Provider) ObjectFetch(app, key string) (io.ReadCloser, error) { + res, err := p.S3.GetObject(&s3.GetObjectInput{ + Bucket: aws.String(p.Bucket), + Key: aws.String(p.objectKey(app, key)), + }) + if ae, ok := err.(awserr.Error); ok && ae.Code() == "NoSuchKey" { + return nil, fmt.Errorf("object not found: %s", key) + } + if err != nil { + return nil, err + } + + return res.Body, nil +} + +func (p *Provider) ObjectList(app, prefix string) ([]string, error) { + res, err := p.S3.ListObjectsV2(&s3.ListObjectsV2Input{ + Bucket: aws.String(p.Bucket), + Delimiter: aws.String("/"), + Prefix: aws.String(p.objectKey(app, prefix)), + }) + if err != nil { + return nil, err + } + + objects := []string{} + + for _, item := range res.Contents { + objects = append(objects, *item.Key) + } + + return objects, nil +} + +// ObjectStore stores an Object +func (p *Provider) ObjectStore(app, key string, r io.Reader, opts structs.ObjectStoreOptions) (*structs.Object, error) { + if key == "" { + k, err := generateTempKey() + if err != nil { + return nil, err + } + key = k + } + + up := s3manager.NewUploaderWithClient(p.S3) + + req := &s3manager.UploadInput{ + Bucket: aws.String(p.Bucket), + Key: aws.String(p.objectKey(app, key)), + Body: r, + } + + if opts.Public != nil && *opts.Public { + req.ACL = aws.String("public-read") + } + + res, err := up.Upload(req) + if err != nil { + return nil, err + } + + url := fmt.Sprintf("object://%s/%s", app, key) + + if opts.Public != nil && *opts.Public { + url = res.Location + } + + o := &structs.Object{Url: url} + + return o, nil +} + +func (p *Provider) objectKey(app, key string) string { + return fmt.Sprintf("%s/%s", app, key) +} + +// func (p *Provider) objectPresignedURL(o *structs.Object, duration time.Duration) (string, error) { +// ou, err := url.Parse(o.Url) +// if err != nil { +// return "", err +// } + +// if ou.Scheme != "object" { +// return "", fmt.Errorf("url is not an object: %s", o.Url) +// } + +// req, _ := p.S3.GetObjectRequest(&s3.GetObjectInput{ +// Bucket: aws.String(p.Bucket), +// Key: aws.String(p.objectKey(ou.Hostname(), ou.Path)), +// }) + +// su, err := req.Presign(duration) +// if err != nil { +// return "", err +// } + +// return su, nil +// } + +func generateTempKey() (string, error) { + data := make([]byte, 1024) + + if _, err := rand.Read(data); err != nil { + return "", err + } + + hash := sha256.Sum256(data) + + return fmt.Sprintf("tmp/%s", hex.EncodeToString(hash[:])[0:30]), nil +} diff --git a/provider/do/repository.go b/provider/do/repository.go new file mode 100644 index 0000000..571aebd --- /dev/null +++ b/provider/do/repository.go @@ -0,0 +1,11 @@ +package do + +import "fmt" + +func (p *Provider) RepositoryAuth(app string) (string, string, error) { + return "docker", p.Secret, nil +} + +func (p *Provider) RepositoryHost(app string) (string, bool, error) { + return fmt.Sprintf("%s/%s", p.Registry, app), true, nil +} diff --git a/provider/do/resolver.go b/provider/do/resolver.go new file mode 100644 index 0000000..d0baaed --- /dev/null +++ b/provider/do/resolver.go @@ -0,0 +1,7 @@ +package do + +import "fmt" + +func (p *Provider) Resolver() (string, error) { + return "", fmt.Errorf("no resolver") +} diff --git a/provider/do/service.go b/provider/do/service.go new file mode 100644 index 0000000..889d7eb --- /dev/null +++ b/provider/do/service.go @@ -0,0 +1,11 @@ +package do + +import ( + "fmt" + + "github.com/convox/convox/pkg/manifest" +) + +func (p *Provider) ServiceHost(app string, s manifest.Service) string { + return fmt.Sprintf("%s.%s.%s", s.Name, app, p.Domain) +} diff --git a/provider/do/system.go b/provider/do/system.go new file mode 100644 index 0000000..a66ae6e --- /dev/null +++ b/provider/do/system.go @@ -0,0 +1,9 @@ +package do + +func (p *Provider) SystemHost() string { + return p.Domain +} + +func (p *Provider) SystemStatus() (string, error) { + return "running", nil +} diff --git a/provider/provider.go b/provider/provider.go index 7d11c27..55703da 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -6,6 +6,7 @@ import ( "github.com/convox/convox/pkg/structs" "github.com/convox/convox/provider/aws" + "github.com/convox/convox/provider/do" "github.com/convox/convox/provider/gcp" "github.com/convox/convox/provider/k8s" ) @@ -18,6 +19,8 @@ func FromEnv() (structs.Provider, error) { switch name { case "aws": return aws.FromEnv() + case "do": + return do.FromEnv() case "gcp": return gcp.FromEnv() case "k8s": diff --git a/terraform/api/do/main.tf b/terraform/api/do/main.tf new file mode 100644 index 0000000..0d04c17 --- /dev/null +++ b/terraform/api/do/main.tf @@ -0,0 +1,48 @@ +terraform { + required_version = ">= 0.12.0" +} + +provider "digitalocean" { + version = "~> 1.9" +} + +provider "kubernetes" { + version = "~> 1.8" + + config_path = var.kubeconfig +} + +locals { + tags = { + System = "convox" + Rack = var.name + } +} + +module "k8s" { + source = "../k8s" + + providers = { + kubernetes = kubernetes + } + + domain = var.domain + kubeconfig = var.kubeconfig + name = var.name + namespace = var.namespace + release = var.release + + annotations = {} + + env = { + BUCKET = digitalocean_spaces_bucket.storage.name + PROVIDER = "do" + REGION = var.region + REGISTRY = "registry.${var.domain}" + ROUTER = var.router + SECRET = var.secret + SPACES_ACCESS = var.access_id + SPACES_ENDPOINT = "https://${var.region}.digitaloceanspaces.com" + SPACES_SECRET = var.secret_key + } +} diff --git a/terraform/api/do/outputs.tf b/terraform/api/do/outputs.tf new file mode 100644 index 0000000..ccfa687 --- /dev/null +++ b/terraform/api/do/outputs.tf @@ -0,0 +1,3 @@ +output "endpoint" { + value = module.k8s.endpoint +} diff --git a/terraform/api/do/registry.tf b/terraform/api/do/registry.tf new file mode 100644 index 0000000..705689f --- /dev/null +++ b/terraform/api/do/registry.tf @@ -0,0 +1 @@ +# data "google_container_registry_repository" "registry" {} diff --git a/terraform/api/do/storage.tf b/terraform/api/do/storage.tf new file mode 100644 index 0000000..8d96264 --- /dev/null +++ b/terraform/api/do/storage.tf @@ -0,0 +1,11 @@ +resource "random_string" "suffix" { + length = 12 + special = false + upper = false +} + +resource "digitalocean_spaces_bucket" "storage" { + name = "${var.name}-storage-${random_string.suffix.result}" + region = var.region + acl = "private" +} diff --git a/terraform/api/do/variables.tf b/terraform/api/do/variables.tf new file mode 100644 index 0000000..5ee5e53 --- /dev/null +++ b/terraform/api/do/variables.tf @@ -0,0 +1,39 @@ +variable "access_id" { + type = "string" +} + +variable "domain" { + type = "string" +} + +variable "kubeconfig" { + type = "string" +} + +variable "name" { + type = "string" +} + +variable "namespace" { + type = "string" +} + +variable "region" { + type = "string" +} + +variable "release" { + type = "string" +} + +variable "router" { + type = "string" +} + +variable "secret" { + type = "string" +} + +variable "secret_key" { + type = "string" +} diff --git a/terraform/api/k8s/main.tf b/terraform/api/k8s/main.tf index e74fd12..eb15225 100644 --- a/terraform/api/k8s/main.tf +++ b/terraform/api/k8s/main.tf @@ -246,7 +246,7 @@ resource "kubernetes_service" "api" { resource "kubernetes_ingress" "api" { metadata { namespace = var.namespace - name = var.name + name = "api" annotations = { "convox.idles" : "true" diff --git a/terraform/cluster/do/kubeconfig.tpl b/terraform/cluster/do/kubeconfig.tpl new file mode 100644 index 0000000..e9348b3 --- /dev/null +++ b/terraform/cluster/do/kubeconfig.tpl @@ -0,0 +1,18 @@ +apiVersion: v1 +clusters: +- cluster: + certificate-authority-data: ${ca} + server: ${endpoint} + name: do +contexts: +- context: + cluster: do + user: do + name: do +current-context: do +kind: Config +preferences: {} +users: +- name: do + user: + token: ${token} diff --git a/terraform/cluster/do/main.tf b/terraform/cluster/do/main.tf new file mode 100644 index 0000000..02dddb0 --- /dev/null +++ b/terraform/cluster/do/main.tf @@ -0,0 +1,154 @@ +terraform { + required_version = ">= 0.12.0" +} + +provider "digitalocean" { + version = "~> 1.9" +} + +provider "local" { + version = "~> 1.3" +} + +provider "random" { + version = "~> 2.2" +} + +resource "digitalocean_kubernetes_cluster" "rack" { + name = var.name + region = var.region + version = "1.14.8-do.0" + + node_pool { + name = "rack" + size = var.node_type + node_count = 3 + } +} + +# data "google_client_config" "current" {} + +# data "google_container_engine_versions" "available" { +# location = data.google_client_config.current.region +# version_prefix = "1.14." +# } + +# data "google_project" "current" {} + +# resource "random_string" "password" { +# length = 64 +# special = true +# } + +# resource "google_container_cluster" "rack" { +# provider = "google-beta" + +# name = var.name +# location = data.google_client_config.current.region + +# remove_default_node_pool = true +# initial_node_count = 1 +# logging_service = "logging.googleapis.com" +# min_master_version = data.google_container_engine_versions.available.latest_master_version + +# ip_allocation_policy { +# use_ip_aliases = true +# } + +# workload_identity_config { +# identity_namespace = "${data.google_project.current.project_id}.svc.id.goog" +# } + +# master_auth { +# username = "gcloud" +# password = random_string.password.result + +# client_certificate_config { +# issue_client_certificate = true +# } +# } +# } + +# resource "google_container_node_pool" "rack" { +# provider = "google-beta" + +# name = "${google_container_cluster.rack.name}-nodes-${var.node_type}" +# location = google_container_cluster.rack.location +# cluster = google_container_cluster.rack.name +# node_count = 1 + +# node_config { +# preemptible = true +# machine_type = var.node_type + +# metadata = { +# disable-legacy-endpoints = "true" +# } + +# workload_metadata_config { +# node_metadata = "GKE_METADATA_SERVER" +# } + +# service_account = google_service_account.nodes.email + +# oauth_scopes = [ +# "https://www.googleapis.com/auth/cloud-platform", +# "https://www.googleapis.com/auth/devstorage.read_write", +# "https://www.googleapis.com/auth/logging.write", +# "https://www.googleapis.com/auth/monitoring", +# ] +# } + +# lifecycle { +# create_before_destroy = true +# } +# } + +resource "local_file" "kubeconfig" { + depends_on = [ + kubernetes_cluster_role_binding.client, + digitalocean_kubernetes_cluster.rack, + ] + + filename = pathexpand("~/.kube/config.do.${var.name}") + content = templatefile("${path.module}/kubeconfig.tpl", { + ca = digitalocean_kubernetes_cluster.rack.kube_config[0].cluster_ca_certificate + endpoint = digitalocean_kubernetes_cluster.rack.endpoint + token = digitalocean_kubernetes_cluster.rack.kube_config[0].token + }) + + lifecycle { + ignore_changes = [content] + } +} + +provider "kubernetes" { + version = "~> 1.8" + + alias = "direct" + + load_config_file = false + + cluster_ca_certificate = "${base64decode(digitalocean_kubernetes_cluster.rack.kube_config[0].cluster_ca_certificate)}" + host = digitalocean_kubernetes_cluster.rack.endpoint + token = digitalocean_kubernetes_cluster.rack.kube_config[0].token +} + +resource "kubernetes_cluster_role_binding" "client" { + provider = "kubernetes.direct" + + metadata { + name = "client-binding" + } + + role_ref { + api_group = "rbac.authorization.k8s.io" + kind = "ClusterRole" + name = "cluster-admin" + } + + subject { + kind = "User" + name = "client" + } +} diff --git a/terraform/cluster/do/outputs.tf b/terraform/cluster/do/outputs.tf new file mode 100644 index 0000000..34390a8 --- /dev/null +++ b/terraform/cluster/do/outputs.tf @@ -0,0 +1,8 @@ +output "kubeconfig" { + depends_on = [ + local_file.kubeconfig, + kubernetes_cluster_role_binding.client, + digitalocean_kubernetes_cluster.rack, + ] + value = local_file.kubeconfig.filename +} diff --git a/terraform/cluster/do/variables.tf b/terraform/cluster/do/variables.tf new file mode 100644 index 0000000..a31ec64 --- /dev/null +++ b/terraform/cluster/do/variables.tf @@ -0,0 +1,11 @@ +variable "name" { + type = string +} + +variable "node_type" { + type = string +} + +variable "region" { + type = string +} diff --git a/terraform/rack/do/main.tf b/terraform/rack/do/main.tf new file mode 100644 index 0000000..6fcd94f --- /dev/null +++ b/terraform/rack/do/main.tf @@ -0,0 +1,60 @@ +terraform { + required_version = ">= 0.12.0" +} + +provider "digitalocean" { + version = "~> 1.9" +} + +provider "kubernetes" { + version = "~> 1.9" + + config_path = var.kubeconfig +} + +module "k8s" { + source = "../k8s" + + providers = { + kubernetes = kubernetes + } + + domain = module.router.endpoint + kubeconfig = var.kubeconfig + name = var.name + release = var.release +} + +module "api" { + source = "../../api/do" + + providers = { + digitalocean = digitalocean + kubernetes = kubernetes + } + + access_id = var.access_id + domain = module.router.endpoint + kubeconfig = var.kubeconfig + name = var.name + namespace = module.k8s.namespace + region = var.region + release = var.release + router = module.router.endpoint + secret = random_string.secret.result + secret_key = var.secret_key +} + +module "router" { + source = "../../router/do" + + providers = { + digitalocean = digitalocean + kubernetes = kubernetes + } + + name = var.name + namespace = module.k8s.namespace + region = var.region + release = var.release +} diff --git a/terraform/rack/do/outputs.tf b/terraform/rack/do/outputs.tf new file mode 100644 index 0000000..4412fd9 --- /dev/null +++ b/terraform/rack/do/outputs.tf @@ -0,0 +1,7 @@ +output "api" { + value = module.api.endpoint +} + +output "endpoint" { + value = module.router.endpoint +} diff --git a/terraform/rack/do/registry.tf b/terraform/rack/do/registry.tf new file mode 100644 index 0000000..7293e58 --- /dev/null +++ b/terraform/rack/do/registry.tf @@ -0,0 +1,174 @@ +resource "random_string" "suffix" { + length = 12 + special = false + upper = false +} + +resource "digitalocean_spaces_bucket" "registry" { + name = "${var.name}-registry-${random_string.suffix.result}" + region = var.region + acl = "private" +} + +resource "random_string" "secret" { + length = 30 +} + +resource "kubernetes_deployment" "registry" { + metadata { + namespace = module.k8s.namespace + name = "registry" + + labels = { + serivce = "registry" + } + } + + spec { + min_ready_seconds = 1 + revision_history_limit = 0 + + selector { + match_labels = { + system = "convox" + service = "registry" + } + } + + strategy { + type = "RollingUpdate" + rolling_update { + max_surge = 1 + max_unavailable = 0 + } + } + + template { + metadata { + labels = { + system = "convox" + service = "registry" + } + } + + spec { + container { + name = "main" + image = "registry:2" + image_pull_policy = "IfNotPresent" + + env { + name = "REGISTRY_HTTP_SECRET" + value = random_string.secret.result + } + + env { + name = "REGISTRY_STORAGE" + value = "s3" + } + + env { + name = "REGISTRY_STORAGE_S3_ACCESSKEY" + value = var.access_id + } + + env { + name = "REGISTRY_STORAGE_S3_BUCKET" + value = digitalocean_spaces_bucket.registry.name + } + + env { + name = "REGISTRY_STORAGE_S3_REGION" + value = var.region + } + + env { + name = "REGISTRY_STORAGE_S3_REGIONENDPOINT" + value = "https://${var.region}.digitaloceanspaces.com" + } + + env { + name = "REGISTRY_STORAGE_S3_SECRETKEY" + value = var.secret_key + } + + port { + container_port = 5000 + protocol = "TCP" + } + + volume_mount { + name = "registry" + mount_path = "/var/lib/registry" + } + } + + volume { + name = "registry" + + host_path { + path = "/var/lib/registry" + } + } + } + } + } +} + +resource "kubernetes_service" "registry" { + metadata { + namespace = module.k8s.namespace + name = "registry" + } + + spec { + type = "ClusterIP" + + selector = { + system = "convox" + service = "registry" + } + + port { + name = "http" + port = 80 + target_port = 5000 + protocol = "TCP" + } + } +} +resource "kubernetes_ingress" "registry" { + metadata { + namespace = module.k8s.namespace + name = "registry" + + annotations = { + "convox.idles" : "true" + } + + labels = { + system = "convox" + service = "registry" + } + } + + spec { + tls { + hosts = ["registry.${module.router.endpoint}"] + } + + rule { + host = "registry.${module.router.endpoint}" + + http { + path { + backend { + service_name = kubernetes_service.registry.metadata.0.name + service_port = 80 + } + } + } + } + } +} + diff --git a/terraform/rack/do/variables.tf b/terraform/rack/do/variables.tf new file mode 100644 index 0000000..0d3ebdf --- /dev/null +++ b/terraform/rack/do/variables.tf @@ -0,0 +1,23 @@ +variable "access_id" { + type = "string" +} + +variable "kubeconfig" { + type = "string" +} + +variable "name" { + type = "string" +} + +variable "region" { + type = "string" +} + +variable "release" { + type = "string" +} + +variable "secret_key" { + type = "string" +} diff --git a/terraform/router/do/main.tf b/terraform/router/do/main.tf new file mode 100644 index 0000000..392705f --- /dev/null +++ b/terraform/router/do/main.tf @@ -0,0 +1,74 @@ +terraform { + required_version = ">= 0.12.0" +} + +provider "digitalocean" { + version = "~> 1.9" +} + +provider "kubernetes" { + version = "~> 1.9" +} + +locals { + tags = { + System = "convox" + Rack = var.name + } +} + +module "k8s" { + source = "../k8s" + + providers = { + kubernetes = kubernetes + } + + namespace = var.namespace + release = var.release + + env = { + CACHE = "redis" + REDIS_ADDR = "${digitalocean_database_cluster.cache.private_host}:${digitalocean_database_cluster.cache.port}" + REDIS_AUTH = digitalocean_database_cluster.cache.password + REDIS_SECURE = "true" + } +} + +resource "kubernetes_service" "router" { + metadata { + namespace = var.namespace + name = "router" + } + + spec { + type = "LoadBalancer" + + port { + name = "http" + port = 80 + protocol = "TCP" + target_port = 80 + } + + port { + name = "https" + port = 443 + protocol = "TCP" + target_port = 443 + } + + selector = { + system = "convox" + service = "router" + } + } + + lifecycle { + ignore_changes = [metadata[0].annotations] + } +} + +data "http" "alias" { + url = "https://alias.convox.com/alias/${kubernetes_service.router.load_balancer_ingress.0.ip}" +} diff --git a/terraform/router/do/outputs.tf b/terraform/router/do/outputs.tf new file mode 100644 index 0000000..46bc596 --- /dev/null +++ b/terraform/router/do/outputs.tf @@ -0,0 +1,4 @@ +output "endpoint" { + value = data.http.alias.body +} + diff --git a/terraform/router/do/redis.tf b/terraform/router/do/redis.tf new file mode 100644 index 0000000..63a6f03 --- /dev/null +++ b/terraform/router/do/redis.tf @@ -0,0 +1,7 @@ +resource "digitalocean_database_cluster" "cache" { + name = "${var.name}-router" + engine = "redis" + size = "db-s-1vcpu-1gb" + region = var.region + node_count = 1 +} diff --git a/terraform/router/do/variables.tf b/terraform/router/do/variables.tf new file mode 100644 index 0000000..0b14755 --- /dev/null +++ b/terraform/router/do/variables.tf @@ -0,0 +1,15 @@ +variable "name" { + type = "string" +} + +variable "namespace" { + type = "string" +} + +variable "region" { + type = "string" +} + +variable "release" { + type = "string" +} diff --git a/terraform/system/do/main.tf b/terraform/system/do/main.tf new file mode 100644 index 0000000..e98cdb7 --- /dev/null +++ b/terraform/system/do/main.tf @@ -0,0 +1,50 @@ +terraform { + required_version = ">= 0.12.0" +} + +provider "digitalocean" { + version = "~> 1.9" +} + +provider "kubernetes" { + version = "~> 1.9" + + config_path = module.cluster.kubeconfig +} + +data "http" "releases" { + url = "https://api.github.com/repos/convox/convox/releases" +} + +locals { + current = jsondecode(data.http.releases.body).0.tag_name + release = coalesce(var.release, local.current) +} + +module "cluster" { + source = "../../cluster/do" + + providers = { + digitalocean = digitalocean + } + + name = var.name + node_type = var.node_type + region = var.region +} + +module "rack" { + source = "../../rack/do" + + providers = { + digitalocean = digitalocean + kubernetes = kubernetes + } + + access_id = var.access_id + kubeconfig = module.cluster.kubeconfig + name = var.name + region = var.region + release = local.release + secret_key = var.secret_key +} diff --git a/terraform/system/do/outputs.tf b/terraform/system/do/outputs.tf new file mode 100644 index 0000000..c229346 --- /dev/null +++ b/terraform/system/do/outputs.tf @@ -0,0 +1,7 @@ +output "api" { + value = module.rack.api +} + +output "endpoint" { + value = module.rack.endpoint +} diff --git a/terraform/system/do/variables.tf b/terraform/system/do/variables.tf new file mode 100644 index 0000000..9315810 --- /dev/null +++ b/terraform/system/do/variables.tf @@ -0,0 +1,23 @@ +variable "access_id" { + type = "string" +} + +variable "name" { + type = "string" +} + +variable "node_type" { + type = "string" +} + +variable "region" { + type = "string" +} + +variable "release" { + default = "" +} + +variable "secret_key" { + type = "string" +}