From 1f9569b76d88eca630e824d38cba1b6058cb20ea Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Tue, 10 Oct 2023 01:41:49 +0300 Subject: [PATCH] singleton --- README.md | 8 +++-- api.w | 1 + containers.w | 4 +-- test/containers.test.w | 16 ++++++++- test/util.js | 22 ++++-------- tf-aws/eks.w | 76 ++++++++++++++++++++++++------------------ tf-aws/util.w | 5 +++ tf-aws/workload.w | 73 +++++++++++++++++++--------------------- 8 files changed, 112 insertions(+), 93 deletions(-) create mode 100644 tf-aws/util.w diff --git a/README.md b/README.md index 178e38f..9f5757a 100644 --- a/README.md +++ b/README.md @@ -42,15 +42,17 @@ cluster. See [Captain's Log](https://winglang.slack.com/archives/C047QFSUL5R/p1696868156845019) in the [Wing Slack](https://t.winglang.io). +- [x] EKS as a singleton - [ ] Publish the library - [ ] Implement `start()` and `stop()` and `url()`. - [ ] Add support for local Dockerfiles (currently only images from Docker Hub are supported), this - includes publishing into an ECR. + includes publishing into an ECR. - [ ] Add support for sidecar containers - [ ] Domains - [ ] SSL -- [ ] Add support for ingress routes (currently all routes go to the container). -- [ ] Use Fargate profiles in EKS instead of managed node groups. +- [ ] What happens if I deploy more than one app into the cluster? Add support for ingress routes + (currently all routes go to the container). +- [ ] Nodes - what should we do there? Use Fargate profiles in EKS instead of managed node groups? - [ ] Open bugs - [ ] Allow referencing an existing EKS cluster. diff --git a/api.w b/api.w index 1ffec59..8392498 100644 --- a/api.w +++ b/api.w @@ -14,6 +14,7 @@ struct ContainerOpts { port: num?; env: Map?; readiness: str?; // http get + replicas: num?; } struct WorkloadProps extends ContainerOpts { diff --git a/containers.w b/containers.w index f3e6237..cdfd9b5 100644 --- a/containers.w +++ b/containers.w @@ -10,9 +10,9 @@ class Workload impl api.IWorkload { let target = util.env("WING_TARGET"); if target == "sim" { - this.inner = new sim.Workload(props); + this.inner = new sim.Workload(props) as this.node.id; } elif target == "tf-aws" { - this.inner = new aws.Workload(props); + this.inner = new aws.Workload(props) as this.node.id; } else { throw "unsupported target ${target}"; } diff --git a/test/containers.test.w b/test/containers.test.w index 9174f9e..ce92b6b 100644 --- a/test/containers.test.w +++ b/test/containers.test.w @@ -7,10 +7,24 @@ let hello = new containers.Workload( image: "paulbouwer/hello-kubernetes:1", port: 8080, readiness: "/", + replicas: 4, env: { "MESSAGE" => message, } -); +) as "hello"; + +// new containers.Workload( +// image: "gcr.io/google-samples/gb-frontend:v4", +// env: { +// "GET_HOSTS_FROM" => "dns", +// }, +// port: 80, +// ) as "guestbook"; + +// new containers.Workload( +// image: "registry.k8s.io/redis:e2e", +// port: 6379, +// ) as "redis"; let getBody = inflight (): str? => { if let url = hello.url() { diff --git a/test/util.js b/test/util.js index 0468a79..54c3b12 100644 --- a/test/util.js +++ b/test/util.js @@ -3,7 +3,7 @@ const cdk8s = require('cdk8s'); const fs = require('fs'); const os = require('os'); const path = require('path'); -const tfaws = require('@winglang/sdk/lib/target-tf-aws'); +const cdktf = require('cdktf'); exports.shell = async function (command, args, cwd) { return new Promise((resolve, reject) => { @@ -24,13 +24,14 @@ exports.entrypointDir = function (scope) { exports.toHelmChart = function(chart) { const app = cdk8s.App.of(chart); + app.resolvers = [new cdk8s.LazyResolver(), new cdk8s.ImplicitTokenResolver(), new cdk8s.NumberStringUnionResolver()]; const docs = cdk8s.App._synthChart(chart); + const yaml = cdk8s.Yaml.stringify(...docs); const workdir = fs.mkdtempSync(path.join(os.tmpdir(), "helm.")); - const templates = fs.mkdirSync(path.join(workdir, "templates"), {recursive: true}); - - const yaml = cdk8s.Yaml.stringify(...docs); + const templates = path.join(workdir, "templates"); + fs.mkdirSync(templates, {recursive: true}); fs.writeFileSync(path.join(templates, "all.yaml"), yaml); const manifest = { @@ -47,14 +48,5 @@ exports.toHelmChart = function(chart) { return workdir; }; -exports.awsRegion = function(scope) { - return tfaws.App.of(scope).region; -} - -// exports.awsVpc = function(scope) { -// return tfaws.App.of(scope).vpc; -// } - -// exports.toSubnet = function(scope) { -// return scope; -// } \ No newline at end of file +exports.toEksCluster = x => x; +exports.toResource = x => x; diff --git a/tf-aws/eks.w b/tf-aws/eks.w index c577105..e8c33d7 100644 --- a/tf-aws/eks.w +++ b/tf-aws/eks.w @@ -1,17 +1,28 @@ bring aws; bring cloud; +bring "constructs" as c; bring "cdktf" as cdktf; bring "@cdktf/provider-aws" as tfaws; bring "@cdktf/provider-helm" as helm4; bring "@cdktf/provider-kubernetes" as kubernetes; bring "./vpc.w" as v; +bring "./util.w" as util2; class EksCluster { + /** singleton */ + pub static getOrCreate(scope: std.IResource): EksCluster { + let stack = cdktf.TerraformStack.of(scope); + let uid = "WingEksCluster"; + return EksCluster.toEksCluster(stack.node.tryFindChild(uid)) ?? new EksCluster() as uid in EksCluster.toResource(stack); + } + pub endpoint: str; pub certificate: str; pub name: str; pub oidcProviderArn: str; + vpc: v.Vpc; + init() { let clusterName = "wing-eks-${this.node.addr.substring(0, 6)}"; @@ -23,7 +34,7 @@ class EksCluster { publicSubnetTags.set("kubernetes.io/role/elb", "1"); publicSubnetTags.set("kubernetes.io/cluster/${clusterName}", "shared"); - let vpc = new v.Vpc( + this.vpc = new v.Vpc( privateSubnetTags: privateSubnetTags.copy(), publicSubnetTags: publicSubnetTags.copy(), ); @@ -34,33 +45,29 @@ class EksCluster { variables: { cluster_name: clusterName, - vpc_id: vpc.id, - subnet_ids: vpc.privateSubnets, + vpc_id: this.vpc.id, + subnet_ids: this.vpc.privateSubnets, cluster_endpoint_public_access: true, - // create_aws_auth_configmap: true, - // manage_aws_auth_configmap: true, eks_managed_node_group_defaults: { ami_type: "AL2_x86_64" }, eks_managed_node_groups: { - one: { + small: { name: "node-group-1", instance_types: ["t3.small"], min_size: 1, - max_size: 3, - desired_size: 2 - }, - two: { - name: "node-group-2", - instance_types: ["t3.small"], - min_size: 1, - max_size: 2, - desired_size: 1, + max_size: 10, + desired_size: 5 }, } } ) as "eks"; + this.name = clusterName; + this.certificate = eks.get("cluster_certificate_authority_data"); + this.endpoint = eks.get("cluster_endpoint"); + this.oidcProviderArn = eks.get("oidc_provider_arn"); + let ebsCsiPolicy = new tfaws.dataAwsIamPolicy.DataAwsIamPolicy(arn: "arn:aws:iam::aws:policy/service-role/AmazonEBSCSIDriverPolicy"); let irsaEbsCsi = new cdktf.TerraformHclModule( @@ -86,11 +93,8 @@ class EksCluster { }, ); - this.name = clusterName; - this.certificate = eks.get("cluster_certificate_authority_data"); - this.endpoint = eks.get("cluster_endpoint"); - this.oidcProviderArn = eks.get("oidc_provider_arn"); + // setup the helm and k8s terraform providers let k8sconfig = { host: this.endpoint, clusterCaCertificate: cdktf.Fn.base64decode(this.certificate), @@ -104,6 +108,22 @@ class EksCluster { new helm4.provider.HelmProvider(kubernetes: k8sconfig); new kubernetes.provider.KubernetesProvider(k8sconfig); + // output the cluster name + new cdktf.TerraformOutput(value: clusterName); + + // install the LB controller to support ingress + this.addLoadBalancerController(); + } + + /** + * Deploys a Helm chart to the cluster. + */ + pub addChart(release: helm4.release.ReleaseConfig) { + new helm4.release.Release(release) as release.name; + } + + addLoadBalancerController() { + let region = new tfaws.dataAwsRegion.DataAwsRegion(); let serviceAccountName = "aws-load-balancer-controller"; let lbRole = new cdktf.TerraformHclModule( source: "terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks", @@ -134,8 +154,6 @@ class EksCluster { } ); - new cdktf.TerraformOutput(value: clusterName); - this.addChart( name: "aws-load-balancer-controller", repository: "https://aws.github.io/eks-charts", @@ -143,8 +161,8 @@ class EksCluster { namespace: "kube-system", dependsOn: [serviceAccount], set: [ - { name: "region", value: EksUtil.awsRegion(this) }, - { name: "vpcId", value: vpc.id }, + { name: "region", value: region.name }, + { name: "vpcId", value: this.vpc.id }, { name: "serviceAccount.create", value: "false" }, { name: "serviceAccount.name", value: serviceAccountName }, { name: "clusterName", value: this.name }, @@ -152,14 +170,6 @@ class EksCluster { ); } - /** - * Deploys a Helm chart to the cluster. - */ - pub addChart(release: helm4.release.ReleaseConfig) { - new helm4.release.Release(release) as release.name; - } + extern "./util.js" pub static toEksCluster(scope: c.IConstruct?): EksCluster?; + extern "./util.js" pub static toResource(scope: c.IConstruct): EksCluster; } - -class EksUtil { - extern "./util.js" pub static awsRegion(scope: std.IResource): str; -} \ No newline at end of file diff --git a/tf-aws/util.w b/tf-aws/util.w new file mode 100644 index 0000000..f95d90c --- /dev/null +++ b/tf-aws/util.w @@ -0,0 +1,5 @@ +bring "cdk8s" as k8s9; + +class Util { + extern "./util.js" pub static toHelmChart(chart: k8s9.Chart): str; +} \ No newline at end of file diff --git a/tf-aws/workload.w b/tf-aws/workload.w index e273457..1feb492 100644 --- a/tf-aws/workload.w +++ b/tf-aws/workload.w @@ -2,9 +2,37 @@ bring "../api.w" as api; bring "./eks.w" as eks; bring "cdk8s-plus-27" as cdk8s; bring "cdk8s" as k8s; +bring "cdktf" as cdktf3; +bring "./util.w" as util; -class _Chart extends k8s.Chart { +class Workload impl api.IWorkload { init(props: api.WorkloadProps) { + let name = "${this.node.id.replace(".", "-")}-${this.node.addr.substring(0, 6)}"; + let cluster = eks.EksCluster.getOrCreate(this); + let chart = new _Chart(name, props); + let helmDir = util.toHelmChart(chart); + + cluster.addChart( + name: name, + chart: helmDir, + ); + } + + pub inflight start() { + throw "Not implemented yet"; + } + + pub inflight stop() { + throw "Not implemented yet"; + } + + pub inflight url(): str? { + throw "Not implemented yet"; + } +} + +class _Chart extends k8s.Chart { + init(name: str, props: api.WorkloadProps) { let env = props.env ?? {}; let envVariables = MutMap{}; @@ -27,9 +55,10 @@ class _Chart extends k8s.Chart { } let deployment = new cdk8s.Deployment( + replicas: props.replicas, metadata: { - name: "deployment-${this.node.addr}" - } + name: name + }, ); deployment.addContainer( @@ -43,13 +72,13 @@ class _Chart extends k8s.Chart { ); let service = deployment.exposeViaService( - name: "service-${this.node.addr}", + name: name, serviceType: cdk8s.ServiceType.NODE_PORT, ); let ingress = new cdk8s.Ingress( metadata: { - name: "ingress-${this.node.addr}", + name: name, annotations: { "kubernetes.io/ingress.class": "alb", "alb.ingress.kubernetes.io/scheme": "internet-facing", @@ -57,39 +86,5 @@ class _Chart extends k8s.Chart { }, defaultBackend: cdk8s.IngressBackend.fromService(service), ); - - // ingress.addRule("/", ); - } -} - -class Workload impl api.IWorkload { - init(props: api.WorkloadProps) { - let cluster = new eks.EksCluster(); - - let chart = new _Chart(props); - let helmDir = Util.toHelmChart(chart); - - // log(helmDir); - - cluster.addChart( - name: "app-${this.node.addr}", - chart: helmDir, - ); } - - pub inflight start() { - throw "Not implemented yet"; - } - - pub inflight stop() { - throw "Not implemented yet"; - } - - pub inflight url(): str? { - throw "Not implemented yet"; - } -} - -class Util { - extern "./util.js" pub static toHelmChart(chart: k8s.Chart): str; } \ No newline at end of file