diff --git a/.github/workflows/containers-pull.yaml b/.github/workflows/containers-pull.yaml index 840f4fb5..46f84a9d 100644 --- a/.github/workflows/containers-pull.yaml +++ b/.github/workflows/containers-pull.yaml @@ -22,7 +22,17 @@ jobs: run: npm install --include=dev working-directory: containers - name: Test - run: wing test + run: | + #!/bin/sh + if [ -n "$CI" ]; then + snapshot_mode="assert" + else + snapshot_mode="update" + fi + + DEBUG=1 wing test + wing test -t tf-aws -s $snapshot_mode containers.test.w + wing test -t tf-aws -s $snapshot_mode containers-with-readiness.test.w working-directory: containers - name: Pack run: wing pack diff --git a/.github/workflows/containers-release.yaml b/.github/workflows/containers-release.yaml index 43164dc6..962c9673 100644 --- a/.github/workflows/containers-release.yaml +++ b/.github/workflows/containers-release.yaml @@ -25,7 +25,17 @@ jobs: run: npm install --include=dev working-directory: containers - name: Test - run: wing test + run: | + #!/bin/sh + if [ -n "$CI" ]; then + snapshot_mode="assert" + else + snapshot_mode="update" + fi + + DEBUG=1 wing test + wing test -t tf-aws -s $snapshot_mode containers.test.w + wing test -t tf-aws -s $snapshot_mode containers-with-readiness.test.w working-directory: containers - name: Pack run: wing pack diff --git a/containers/containers-with-readiness.test.w b/containers/containers-with-readiness.test.w new file mode 100644 index 00000000..8fc09d0a --- /dev/null +++ b/containers/containers-with-readiness.test.w @@ -0,0 +1,32 @@ +bring "./workload.w" as containers; +bring expect; +bring http; + +let message = "hello, wing change!!"; + +let hello = new containers.Workload( + name: "hello", + image: "paulbouwer/hello-kubernetes:1", + port: 8080, + readiness: "/", + replicas: 2, + env: { + "MESSAGE" => message, + }, + public: true, +) as "hello"; + + +let httpGet = inflight (url: str?): str => { + if let url = url { + return http.get(url).body; + } + + throw "no body"; +}; + +test "access public url" { + let helloBody = httpGet(hello.publicUrl); + log(helloBody); + assert(helloBody.contains(message)); +} diff --git a/containers/containers-with-readiness.test.w.tf-aws.snap.md b/containers/containers-with-readiness.test.w.tf-aws.snap.md new file mode 100644 index 00000000..5efd269d --- /dev/null +++ b/containers/containers-with-readiness.test.w.tf-aws.snap.md @@ -0,0 +1,332 @@ +# `containers-with-readiness.test.w.tf-aws.snap.md` + +## main.tf.json + +```json +{ + "//": { + "metadata": { + "backend": "local", + "stackName": "root", + "version": "0.20.3" + }, + "outputs": { + "root": { + "Default": { + "Default": { + "WingEksCluster": { + "eks.certificate": "WingEksCluster_ekscertificate_183C7367", + "eks.cluster_name": "WingEksCluster_ekscluster_name_E1D79024", + "eks.endpoint": "WingEksCluster_eksendpoint_FD8710BA" + } + } + } + } + } + }, + "data": { + "aws_availability_zones": { + "WingEksCluster_Vpc_DataAwsAvailabilityZones_088D4D6B": { + "//": { + "metadata": { + "path": "root/Default/Default/WingEksCluster/Vpc/DataAwsAvailabilityZones", + "uniqueId": "WingEksCluster_Vpc_DataAwsAvailabilityZones_088D4D6B" + } + }, + "filter": { + "name": "opt-in-status", + "values": [ + "opt-in-not-required" + ] + } + } + }, + "aws_caller_identity": { + "WingAwsUtil_DataAwsCallerIdentity_E989AAD9": { + "//": { + "metadata": { + "path": "root/Default/Default/WingAwsUtil/DataAwsCallerIdentity", + "uniqueId": "WingAwsUtil_DataAwsCallerIdentity_E989AAD9" + } + } + } + }, + "aws_region": { + "WingAwsUtil_DataAwsRegion_EEBA70AA": { + "//": { + "metadata": { + "path": "root/Default/Default/WingAwsUtil/DataAwsRegion", + "uniqueId": "WingAwsUtil_DataAwsRegion_EEBA70AA" + } + } + } + }, + "kubernetes_ingress_v1": { + "hello_DataKubernetesIngressV1_7138FD92": { + "//": { + "metadata": { + "path": "root/Default/Default/hello/hello/DataKubernetesIngressV1", + "uniqueId": "hello_DataKubernetesIngressV1_7138FD92" + } + }, + "depends_on": [ + "helm_release.hello_Release_00E5935C" + ], + "metadata": { + "name": "hello" + }, + "provider": "kubernetes" + } + } + }, + "module": { + "WingEksCluster_Vpc_TerraformHclModule_708526D4": { + "//": { + "metadata": { + "path": "root/Default/Default/WingEksCluster/Vpc/TerraformHclModule", + "uniqueId": "WingEksCluster_Vpc_TerraformHclModule_708526D4" + } + }, + "azs": "${slice(data.aws_availability_zones.WingEksCluster_Vpc_DataAwsAvailabilityZones_088D4D6B.names, 0, 3)}", + "cidr": "10.0.0.0/16", + "enable_dns_hostnames": true, + "enable_nat_gateway": true, + "private_subnet_tags": { + "kubernetes.io/cluster/wing-eks-c888f0": "shared", + "kubernetes.io/role/internal-elb": "1" + }, + "private_subnets": [ + "10.0.1.0/24", + "10.0.2.0/24", + "10.0.3.0/24" + ], + "public_subnet_tags": { + "kubernetes.io/cluster/wing-eks-c888f0": "shared", + "kubernetes.io/role/elb": "1" + }, + "public_subnets": [ + "10.0.4.0/24", + "10.0.5.0/24", + "10.0.6.0/24" + ], + "single_nat_gateway": true, + "source": "terraform-aws-modules/vpc/aws", + "version": "4.0.2" + }, + "WingEksCluster_eks_B53CDB45": { + "//": { + "metadata": { + "path": "root/Default/Default/WingEksCluster/eks", + "uniqueId": "WingEksCluster_eks_B53CDB45" + } + }, + "cluster_addons": { + "coredns": { + "configuration_values": "${jsonencode({\"computeType\" = \"Fargate\"})}" + }, + "kube-proxy": { + }, + "vpc-cni": { + } + }, + "cluster_endpoint_public_access": true, + "cluster_name": "wing-eks-c888f0", + "cluster_version": "1.27", + "create_cluster_security_group": false, + "create_node_security_group": false, + "fargate_profiles": { + "default": { + "name": "default", + "selectors": [ + { + "namespace": "kube-system" + }, + { + "namespace": "default" + } + ] + } + }, + "source": "terraform-aws-modules/eks/aws", + "subnet_ids": "${module.WingEksCluster_Vpc_TerraformHclModule_708526D4.private_subnets}", + "version": "19.17.1", + "vpc_id": "${module.WingEksCluster_Vpc_TerraformHclModule_708526D4.vpc_id}" + }, + "WingEksCluster_lb_role_271BFF6C": { + "//": { + "metadata": { + "path": "root/Default/Default/WingEksCluster/lb_role", + "uniqueId": "WingEksCluster_lb_role_271BFF6C" + } + }, + "attach_load_balancer_controller_policy": true, + "oidc_providers": { + "main": { + "namespace_service_accounts": [ + "kube-system:aws-load-balancer-controller" + ], + "provider_arn": "${module.WingEksCluster_eks_B53CDB45.oidc_provider_arn}" + } + }, + "role_name": "eks-lb-role-c8c8413959d4dc713edd195c49a192ea57f59d59fd", + "source": "terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks" + } + }, + "output": { + "WingEksCluster_ekscertificate_183C7367": { + "description": "eks.certificate", + "value": "${module.WingEksCluster_eks_B53CDB45.cluster_certificate_authority_data}" + }, + "WingEksCluster_ekscluster_name_E1D79024": { + "description": "eks.cluster_name", + "value": "wing-eks-c888f0" + }, + "WingEksCluster_eksendpoint_FD8710BA": { + "description": "eks.endpoint", + "value": "${module.WingEksCluster_eks_B53CDB45.cluster_endpoint}" + } + }, + "provider": { + "aws": [ + { + } + ], + "helm": [ + { + "kubernetes": { + "cluster_ca_certificate": "${base64decode(module.WingEksCluster_eks_B53CDB45.cluster_certificate_authority_data)}", + "exec": { + "api_version": "client.authentication.k8s.io/v1beta1", + "args": [ + "eks", + "get-token", + "--cluster-name", + "wing-eks-c888f0" + ], + "command": "aws" + }, + "host": "${module.WingEksCluster_eks_B53CDB45.cluster_endpoint}" + } + } + ], + "kubernetes": [ + { + "cluster_ca_certificate": "${base64decode(module.WingEksCluster_eks_B53CDB45.cluster_certificate_authority_data)}", + "exec": { + "api_version": "client.authentication.k8s.io/v1beta1", + "args": [ + "eks", + "get-token", + "--cluster-name", + "wing-eks-c888f0" + ], + "command": "aws" + }, + "host": "${module.WingEksCluster_eks_B53CDB45.cluster_endpoint}" + } + ] + }, + "resource": { + "helm_release": { + "WingEksCluster_Release_AE9D9364": { + "//": { + "metadata": { + "path": "root/Default/Default/WingEksCluster/Release", + "uniqueId": "WingEksCluster_Release_AE9D9364" + } + }, + "chart": "aws-load-balancer-controller", + "depends_on": [ + "kubernetes_service_account.WingEksCluster_ServiceAccount_580F8592" + ], + "name": "aws-load-balancer-controller", + "namespace": "kube-system", + "provider": "helm", + "repository": "https://aws.github.io/eks-charts", + "set": [ + { + "name": "region", + "value": "${data.aws_region.WingAwsUtil_DataAwsRegion_EEBA70AA.name}" + }, + { + "name": "vpcId", + "value": "${module.WingEksCluster_Vpc_TerraformHclModule_708526D4.vpc_id}" + }, + { + "name": "serviceAccount.create", + "value": "false" + }, + { + "name": "serviceAccount.name", + "value": "aws-load-balancer-controller" + }, + { + "name": "clusterName", + "value": "wing-eks-c888f0" + } + ] + }, + "hello_Release_00E5935C": { + "//": { + "metadata": { + "path": "root/Default/Default/hello/hello/Release", + "uniqueId": "hello_Release_00E5935C" + } + }, + "chart": ".wing/helm/hello-2dc5ca0312851a277e2c2334138c2f4a", + "depends_on": [ + ], + "name": "hello", + "provider": "helm", + "values": [ + "image: paulbouwer/hello-kubernetes:1" + ] + } + }, + "kubernetes_service_account": { + "WingEksCluster_ServiceAccount_580F8592": { + "//": { + "metadata": { + "path": "root/Default/Default/WingEksCluster/ServiceAccount", + "uniqueId": "WingEksCluster_ServiceAccount_580F8592" + } + }, + "metadata": { + "annotations": { + "eks.amazonaws.com/role-arn": "${module.WingEksCluster_lb_role_271BFF6C.iam_role_arn}", + "eks.amazonaws.com/sts-regional-endpoints": "true" + }, + "labels": { + "app.kubernetes.io/component": "controller", + "app.kubernetes.io/name": "aws-load-balancer-controller" + }, + "name": "aws-load-balancer-controller", + "namespace": "kube-system" + }, + "provider": "kubernetes" + } + } + }, + "terraform": { + "backend": { + "local": { + "path": "./terraform.tfstate" + } + }, + "required_providers": { + "aws": { + "source": "aws", + "version": "5.31.0" + }, + "helm": { + "source": "helm", + "version": "2.12.1" + }, + "kubernetes": { + "source": "kubernetes", + "version": "2.27.0" + } + } + } +} +``` diff --git a/containers/containers.test.w b/containers/containers.test.w index 0b01a385..6aaddb36 100644 --- a/containers/containers.test.w +++ b/containers/containers.test.w @@ -2,20 +2,6 @@ bring "./workload.w" as containers; bring expect; bring http; -let message = "hello, wing change!!"; - -let hello = new containers.Workload( - name: "hello", - image: "paulbouwer/hello-kubernetes:1", - port: 8080, - readiness: "/", - replicas: 2, - env: { - "MESSAGE" => message, - }, - public: true, -) as "hello"; - let echo = new containers.Workload( name: "http-echo", image: "hashicorp/http-echo", @@ -34,10 +20,6 @@ let httpGet = inflight (url: str?): str => { }; test "access public url" { - let helloBody = httpGet(hello.publicUrl); - log(helloBody); - assert(helloBody.contains(message)); - let echoBody = httpGet(echo.publicUrl); assert(echoBody.contains("hello1234")); } diff --git a/containers/containers.test.w.tf-aws.snap.md b/containers/containers.test.w.tf-aws.snap.md new file mode 100644 index 00000000..5ec621f6 --- /dev/null +++ b/containers/containers.test.w.tf-aws.snap.md @@ -0,0 +1,332 @@ +# `containers.test.w.tf-aws.snap.md` + +## main.tf.json + +```json +{ + "//": { + "metadata": { + "backend": "local", + "stackName": "root", + "version": "0.20.3" + }, + "outputs": { + "root": { + "Default": { + "Default": { + "WingEksCluster": { + "eks.certificate": "WingEksCluster_ekscertificate_183C7367", + "eks.cluster_name": "WingEksCluster_ekscluster_name_E1D79024", + "eks.endpoint": "WingEksCluster_eksendpoint_FD8710BA" + } + } + } + } + } + }, + "data": { + "aws_availability_zones": { + "WingEksCluster_Vpc_DataAwsAvailabilityZones_088D4D6B": { + "//": { + "metadata": { + "path": "root/Default/Default/WingEksCluster/Vpc/DataAwsAvailabilityZones", + "uniqueId": "WingEksCluster_Vpc_DataAwsAvailabilityZones_088D4D6B" + } + }, + "filter": { + "name": "opt-in-status", + "values": [ + "opt-in-not-required" + ] + } + } + }, + "aws_caller_identity": { + "WingAwsUtil_DataAwsCallerIdentity_E989AAD9": { + "//": { + "metadata": { + "path": "root/Default/Default/WingAwsUtil/DataAwsCallerIdentity", + "uniqueId": "WingAwsUtil_DataAwsCallerIdentity_E989AAD9" + } + } + } + }, + "aws_region": { + "WingAwsUtil_DataAwsRegion_EEBA70AA": { + "//": { + "metadata": { + "path": "root/Default/Default/WingAwsUtil/DataAwsRegion", + "uniqueId": "WingAwsUtil_DataAwsRegion_EEBA70AA" + } + } + } + }, + "kubernetes_ingress_v1": { + "http-echo_DataKubernetesIngressV1_14943683": { + "//": { + "metadata": { + "path": "root/Default/Default/http-echo/http-echo/DataKubernetesIngressV1", + "uniqueId": "http-echo_DataKubernetesIngressV1_14943683" + } + }, + "depends_on": [ + "helm_release.http-echo_Release_2E4AC011" + ], + "metadata": { + "name": "http-echo" + }, + "provider": "kubernetes" + } + } + }, + "module": { + "WingEksCluster_Vpc_TerraformHclModule_708526D4": { + "//": { + "metadata": { + "path": "root/Default/Default/WingEksCluster/Vpc/TerraformHclModule", + "uniqueId": "WingEksCluster_Vpc_TerraformHclModule_708526D4" + } + }, + "azs": "${slice(data.aws_availability_zones.WingEksCluster_Vpc_DataAwsAvailabilityZones_088D4D6B.names, 0, 3)}", + "cidr": "10.0.0.0/16", + "enable_dns_hostnames": true, + "enable_nat_gateway": true, + "private_subnet_tags": { + "kubernetes.io/cluster/wing-eks-c81852": "shared", + "kubernetes.io/role/internal-elb": "1" + }, + "private_subnets": [ + "10.0.1.0/24", + "10.0.2.0/24", + "10.0.3.0/24" + ], + "public_subnet_tags": { + "kubernetes.io/cluster/wing-eks-c81852": "shared", + "kubernetes.io/role/elb": "1" + }, + "public_subnets": [ + "10.0.4.0/24", + "10.0.5.0/24", + "10.0.6.0/24" + ], + "single_nat_gateway": true, + "source": "terraform-aws-modules/vpc/aws", + "version": "4.0.2" + }, + "WingEksCluster_eks_B53CDB45": { + "//": { + "metadata": { + "path": "root/Default/Default/WingEksCluster/eks", + "uniqueId": "WingEksCluster_eks_B53CDB45" + } + }, + "cluster_addons": { + "coredns": { + "configuration_values": "${jsonencode({\"computeType\" = \"Fargate\"})}" + }, + "kube-proxy": { + }, + "vpc-cni": { + } + }, + "cluster_endpoint_public_access": true, + "cluster_name": "wing-eks-c81852", + "cluster_version": "1.27", + "create_cluster_security_group": false, + "create_node_security_group": false, + "fargate_profiles": { + "default": { + "name": "default", + "selectors": [ + { + "namespace": "kube-system" + }, + { + "namespace": "default" + } + ] + } + }, + "source": "terraform-aws-modules/eks/aws", + "subnet_ids": "${module.WingEksCluster_Vpc_TerraformHclModule_708526D4.private_subnets}", + "version": "19.17.1", + "vpc_id": "${module.WingEksCluster_Vpc_TerraformHclModule_708526D4.vpc_id}" + }, + "WingEksCluster_lb_role_271BFF6C": { + "//": { + "metadata": { + "path": "root/Default/Default/WingEksCluster/lb_role", + "uniqueId": "WingEksCluster_lb_role_271BFF6C" + } + }, + "attach_load_balancer_controller_policy": true, + "oidc_providers": { + "main": { + "namespace_service_accounts": [ + "kube-system:aws-load-balancer-controller" + ], + "provider_arn": "${module.WingEksCluster_eks_B53CDB45.oidc_provider_arn}" + } + }, + "role_name": "eks-lb-role-c8c8413959d4dc713edd195c49a192ea57f59d59fd", + "source": "terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks" + } + }, + "output": { + "WingEksCluster_ekscertificate_183C7367": { + "description": "eks.certificate", + "value": "${module.WingEksCluster_eks_B53CDB45.cluster_certificate_authority_data}" + }, + "WingEksCluster_ekscluster_name_E1D79024": { + "description": "eks.cluster_name", + "value": "wing-eks-c81852" + }, + "WingEksCluster_eksendpoint_FD8710BA": { + "description": "eks.endpoint", + "value": "${module.WingEksCluster_eks_B53CDB45.cluster_endpoint}" + } + }, + "provider": { + "aws": [ + { + } + ], + "helm": [ + { + "kubernetes": { + "cluster_ca_certificate": "${base64decode(module.WingEksCluster_eks_B53CDB45.cluster_certificate_authority_data)}", + "exec": { + "api_version": "client.authentication.k8s.io/v1beta1", + "args": [ + "eks", + "get-token", + "--cluster-name", + "wing-eks-c81852" + ], + "command": "aws" + }, + "host": "${module.WingEksCluster_eks_B53CDB45.cluster_endpoint}" + } + } + ], + "kubernetes": [ + { + "cluster_ca_certificate": "${base64decode(module.WingEksCluster_eks_B53CDB45.cluster_certificate_authority_data)}", + "exec": { + "api_version": "client.authentication.k8s.io/v1beta1", + "args": [ + "eks", + "get-token", + "--cluster-name", + "wing-eks-c81852" + ], + "command": "aws" + }, + "host": "${module.WingEksCluster_eks_B53CDB45.cluster_endpoint}" + } + ] + }, + "resource": { + "helm_release": { + "WingEksCluster_Release_AE9D9364": { + "//": { + "metadata": { + "path": "root/Default/Default/WingEksCluster/Release", + "uniqueId": "WingEksCluster_Release_AE9D9364" + } + }, + "chart": "aws-load-balancer-controller", + "depends_on": [ + "kubernetes_service_account.WingEksCluster_ServiceAccount_580F8592" + ], + "name": "aws-load-balancer-controller", + "namespace": "kube-system", + "provider": "helm", + "repository": "https://aws.github.io/eks-charts", + "set": [ + { + "name": "region", + "value": "${data.aws_region.WingAwsUtil_DataAwsRegion_EEBA70AA.name}" + }, + { + "name": "vpcId", + "value": "${module.WingEksCluster_Vpc_TerraformHclModule_708526D4.vpc_id}" + }, + { + "name": "serviceAccount.create", + "value": "false" + }, + { + "name": "serviceAccount.name", + "value": "aws-load-balancer-controller" + }, + { + "name": "clusterName", + "value": "wing-eks-c81852" + } + ] + }, + "http-echo_Release_2E4AC011": { + "//": { + "metadata": { + "path": "root/Default/Default/http-echo/http-echo/Release", + "uniqueId": "http-echo_Release_2E4AC011" + } + }, + "chart": ".wing/helm/http-echo-273a4641f9f305c101f6b0ae07761c77", + "depends_on": [ + ], + "name": "http-echo", + "provider": "helm", + "values": [ + "image: hashicorp/http-echo" + ] + } + }, + "kubernetes_service_account": { + "WingEksCluster_ServiceAccount_580F8592": { + "//": { + "metadata": { + "path": "root/Default/Default/WingEksCluster/ServiceAccount", + "uniqueId": "WingEksCluster_ServiceAccount_580F8592" + } + }, + "metadata": { + "annotations": { + "eks.amazonaws.com/role-arn": "${module.WingEksCluster_lb_role_271BFF6C.iam_role_arn}", + "eks.amazonaws.com/sts-regional-endpoints": "true" + }, + "labels": { + "app.kubernetes.io/component": "controller", + "app.kubernetes.io/name": "aws-load-balancer-controller" + }, + "name": "aws-load-balancer-controller", + "namespace": "kube-system" + }, + "provider": "kubernetes" + } + } + }, + "terraform": { + "backend": { + "local": { + "path": "./terraform.tfstate" + } + }, + "required_providers": { + "aws": { + "source": "aws", + "version": "5.31.0" + }, + "helm": { + "source": "helm", + "version": "2.12.1" + }, + "kubernetes": { + "source": "kubernetes", + "version": "2.27.0" + } + } + } +} +``` diff --git a/containers/helm.js b/containers/helm.js index 02429b33..d8901056 100644 --- a/containers/helm.js +++ b/containers/helm.js @@ -3,11 +3,19 @@ const fs = require('fs'); const path = require('path'); const crypto = require('crypto'); +class LazyResolver { + resolve(context) { + if (typeof(context.value.produce) === "function") { + const resolved = context.value.produce(); + context.replaceValue(resolved); + } + } +} exports.toHelmChart = function (wingdir, chart) { const app = cdk8s.App.of(chart); - app.resolvers = [new cdk8s.LazyResolver(), new cdk8s.ImplicitTokenResolver(), new cdk8s.NumberStringUnionResolver()]; + app.resolvers = [new LazyResolver(), new cdk8s.LazyResolver(), new cdk8s.ImplicitTokenResolver(), new cdk8s.NumberStringUnionResolver()]; const docs = cdk8s.App._synthChart(chart); const yaml = cdk8s.Yaml.stringify(...docs); diff --git a/containers/helm.w b/containers/helm.w new file mode 100644 index 00000000..540ce26d --- /dev/null +++ b/containers/helm.w @@ -0,0 +1,87 @@ +bring "./api.w" as api; +bring "cdk8s-plus-27" as plus; +bring "cdk8s" as cdk8s; + +pub class Chart extends cdk8s.Chart { + name: str; + + new(props: api.WorkloadProps) { + let env = props.env ?? {}; + let envVariables = MutMap{}; + + for k in env.keys() { + if let v = env[k] { + envVariables.set(k, plus.EnvValue.fromValue(v)); + } + } + + let ports = MutArray[]; + if let port = props.port { + ports.push({ number: port }); + } + + let var readiness: plus.Probe? = nil; + if let x = props.readiness { + if let port = props.port { + readiness = plus.Probe.fromHttpGet(x, port: port); + } else { + throw "Cannot setup readiness probe without a `port`"; + } + } + + let deployment = new plus.Deployment( + replicas: props.replicas, + metadata: { + name: props.name + }, + ); + + deployment.addContainer( + image: #"{{ .Values.image }}", + envVariables: envVariables.copy(), + ports: ports.copy(), + readiness: readiness, + args: props.args, + securityContext: { + ensureNonRoot: false, + } + ); + + let isPublic = props.public ?? false; + let var serviceType: plus.ServiceType? = nil; + + if isPublic { + serviceType = plus.ServiceType.NODE_PORT; + } + + let service = deployment.exposeViaService( + name: props.name, + serviceType: serviceType, + ); + + if isPublic { + new plus.Ingress( + metadata: { + name: props.name, + annotations: { + "kubernetes.io/ingress.class": "alb", + "alb.ingress.kubernetes.io/scheme": "internet-facing", + "alb.ingress.kubernetes.io/target-type": "ip", + "alb.ingress.kubernetes.io/healthcheck-protocol": "HTTP", + "alb.ingress.kubernetes.io/healthcheck-port": "traffic-port", + "alb.ingress.kubernetes.io/healthcheck-path": "/", + } + }, + defaultBackend: plus.IngressBackend.fromService(service), + ); + } + + this.name = props.name; + } + + pub toHelm(workdir: str): str { + return Chart.toHelmChart(workdir, this); + } + + extern "./helm.js" pub static toHelmChart(wingdir: str, chart: cdk8s.Chart): str; +} diff --git a/containers/package-lock.json b/containers/package-lock.json index c34d1748..891c3a8b 100644 --- a/containers/package-lock.json +++ b/containers/package-lock.json @@ -1,12 +1,12 @@ { "name": "@winglibs/containers", - "version": "0.0.28", + "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@winglibs/containers", - "version": "0.0.28", + "version": "0.1.0", "license": "MIT", "peerDependencies": { "@cdktf/provider-aws": "^19.12.0", diff --git a/containers/package.json b/containers/package.json index b9a98c55..c0dd70e1 100644 --- a/containers/package.json +++ b/containers/package.json @@ -1,6 +1,6 @@ { "name": "@winglibs/containers", - "version": "0.0.28", + "version": "0.1.0", "description": "Container support for Wing", "repository": { "type": "git", diff --git a/containers/test.sh b/containers/test.sh new file mode 100755 index 00000000..7298891c --- /dev/null +++ b/containers/test.sh @@ -0,0 +1,10 @@ +#!/bin/sh +if [ -n "$CI" ]; then + snapshot_mode="assert" +else + snapshot_mode="update" +fi + +DEBUG=1 wing test +wing test -t tf-aws -s $snapshot_mode containers.test.w +wing test -t tf-aws -s $snapshot_mode containers-with-readiness.test.w diff --git a/containers/test/microservices_consumer/index.js b/containers/test/microservices_consumer/index.js index e6392f1f..d68e2170 100755 --- a/containers/test/microservices_consumer/index.js +++ b/containers/test/microservices_consumer/index.js @@ -7,7 +7,7 @@ if (!PRODUCER_URL) { process.exit(1); } -process.on('SIGINT', () => { +process.on('SIGTERM', () => { console.info("Interrupted") process.exit(0) }); @@ -30,4 +30,4 @@ const server = http.createServer((req, res) => { }); console.log('listening on port 3000'); -server.listen(3000); \ No newline at end of file +server.listen(3000); diff --git a/containers/test/microservices_producer/index.js b/containers/test/microservices_producer/index.js index 560a1b44..c5513570 100755 --- a/containers/test/microservices_producer/index.js +++ b/containers/test/microservices_producer/index.js @@ -1,7 +1,7 @@ #!/usr/bin/env node const http = require('http'); -process.on('SIGINT', () => { +process.on('SIGTERM', () => { console.info("Interrupted") process.exit(0) }); @@ -13,4 +13,5 @@ const server = http.createServer((req, res) => { }); console.log('listening on port 4000'); -server.listen(4000); \ No newline at end of file +server.listen(4000); + diff --git a/containers/test/my_app/index.js b/containers/test/my_app/index.js index 55e56a40..788f782d 100755 --- a/containers/test/my_app/index.js +++ b/containers/test/my_app/index.js @@ -1,7 +1,7 @@ #!/usr/bin/env node const http = require('http'); -process.on('SIGINT', () => { +process.on('SIGTERM', () => { console.info("Interrupted") process.exit(0) }); diff --git a/containers/tfaws-eks.w b/containers/tfaws-eks.w index 3ea80539..124cfaf8 100644 --- a/containers/tfaws-eks.w +++ b/containers/tfaws-eks.w @@ -5,7 +5,6 @@ bring "cdktf" as cdktf; bring "@cdktf/provider-helm" as helm; bring "@cdktf/provider-kubernetes" as eks; bring "./tfaws-vpc.w" as vpc; -bring "./values.w" as values; bring "./aws.w" as aws_info; struct ClusterAttributes { @@ -23,8 +22,6 @@ interface ICluster extends std.IResource { pub class ClusterBase impl ICluster { pub attributes(): ClusterAttributes { throw "Not implemented"; - // WORKAROUND: Compiler doesn't recognize that this will never return, so we need to return something - return { certificate: "", endpoint: "", name: "" }; } pub kubernetesProvider(): cdktf.TerraformProvider { @@ -81,6 +78,7 @@ class ClusterRef extends ClusterBase impl ICluster { } } + pub class Cluster extends ClusterBase impl ICluster { /** singleton */ @@ -88,11 +86,12 @@ pub class Cluster extends ClusterBase impl ICluster { let root = nodeof(scope).root; let uid = "WingEksCluster"; let existing: ICluster? = unsafeCast(root.node.tryFindChild(uid)); + let newCluster = (): ICluster => { - if let attrs = Cluster.tryGetClusterAttributes() { + if let attrs = Cluster.tryReadParameters(scope) { return new ClusterRef(attrs) as uid in root; } else { - let clusterName = "wing-eks-{std.Node.of(scope).addr.substring(0, 6)}"; + let clusterName = "wing-eks-{nodeof(scope).addr.substring(0, 6)}"; return new Cluster(clusterName) as uid in root; } }; @@ -100,18 +99,15 @@ pub class Cluster extends ClusterBase impl ICluster { return existing ?? newCluster(); } - - static tryGetClusterAttributes(): ClusterAttributes? { - if !values.Values.has("eks.cluster_name") { - return nil; + static tryReadParameters(scope: std.IResource): ClusterAttributes? { + if let json = nodeof(scope).app.parameters.value("eks") { + log(json); + if let attrs = ClusterAttributes.tryParseJson(json) { + return attrs; + } } - return ClusterAttributes { - name: values.Values.get("eks.cluster_name"), - certificate: values.Values.get("eks.certificate"), - endpoint: values.Values.get("eks.endpoint"), - }; - + return nil; } _attributes: ClusterAttributes; diff --git a/containers/values.w b/containers/values.w deleted file mode 100644 index e717579a..00000000 --- a/containers/values.w +++ /dev/null @@ -1,51 +0,0 @@ -bring util; -bring fs; - -pub class Values { - pub static all(): Map { - let all = MutMap{}; - - if let valuesFile = util.tryEnv("WING_VALUES_FILE") { - if valuesFile != "undefined" { // bug - if !fs.exists(valuesFile) { - throw "Values file {valuesFile} not found"; - } - - for x in fs.readYaml(valuesFile) { - for entry in Json.entries(x) { - all.set(entry.key, entry.value.asStr()); - } - } - } - } - - if let values = util.tryEnv("WING_VALUES") { - if values != "undefined" { - for v in values.split(",") { - let kv = v.split("="); - let key = kv[0]; - let value = kv[1]; - all.set(key, value); - } - } - } - - return all.copy(); - } - - pub static tryGet(key: str): str? { - return Values.all().tryGet(key); - } - - pub static has(key: str): bool { - return Values.tryGet(key)?; - } - - pub static get(key: str): str { - if let value = Values.tryGet(key) { - return value; - } else { - throw "Missing platform value '{key}' (use --values or -v)"; - } - } -} diff --git a/containers/wing.toml b/containers/wing.toml new file mode 100644 index 00000000..1201cfb6 --- /dev/null +++ b/containers/wing.toml @@ -0,0 +1,2 @@ +[containers] +provider="eks" diff --git a/containers/workload.sim.w b/containers/workload.sim.w index 0a4813c9..2c564e84 100644 --- a/containers/workload.sim.w +++ b/containers/workload.sim.w @@ -36,27 +36,31 @@ pub class Workload_sim { return this.publicUrl!; }); - let s1 = new cloud.Service(inflight () => { - state.set(publicUrlKey, "http://localhost:{c.hostPort!}"); - state.set(internalUrlKey, "http://host.docker.internal:{c.hostPort!}"); - }) as "urls"; + let readiness = new cloud.Service(inflight () => { + let publicUrl = "http://localhost:{c.hostPort!}"; - let s2 = new cloud.Service(inflight () => { if let readiness = props.readiness { - let readinessUrl = "{this.publicUrl!}{readiness}"; - log("waiting for container to be ready: {readinessUrl}..."); + // if we have a readiness check, wait for the container to be ready + let readinessUrl = "{publicUrl}{readiness}"; + util.waitUntil(inflight () => { try { + log("Readiness check: GET {readinessUrl}"); return http.get(readinessUrl).ok; } catch { return false; } - }, interval: 0.1s); + }, interval: 0.5s); + + log("Container is ready!"); } + + // ready! + state.set(publicUrlKey, publicUrl); + state.set(internalUrlKey, "http://host.docker.internal:{c.hostPort!}"); }) as "readiness"; - nodeof(s1).hidden = true; - nodeof(s2).hidden = true; + nodeof(readiness).hidden = true; } } diff --git a/containers/workload.tfaws.w b/containers/workload.tfaws.w index cfb6ba98..33a0326c 100644 --- a/containers/workload.tfaws.w +++ b/containers/workload.tfaws.w @@ -5,7 +5,8 @@ bring "cdk8s" as cdk8s; bring "cdktf" as cdktf; bring "./tfaws-ecr.w" as ecr; bring "@cdktf/provider-kubernetes" as k8s; -bring "@cdktf/provider-helm" as helm; +bring "@cdktf/provider-helm" as helm_provider; +bring "./helm.w" as helm; bring fs; pub class Workload_tfaws { @@ -14,7 +15,6 @@ pub class Workload_tfaws { new(props: api.WorkloadProps) { let cluster = eks.Cluster.getOrCreate(this); - let var image = props.image; let var deps = MutArray[]; @@ -33,99 +33,13 @@ pub class Workload_tfaws { } } - class _Chart extends cdk8s.Chart { - name: str; - - new(props: api.WorkloadProps) { - let env = props.env ?? {}; - let envVariables = MutMap{}; - - for k in env.keys() { - if let v = env[k] { - envVariables.set(k, plus.EnvValue.fromValue(v)); - } - } - - let ports = MutArray[]; - if let port = props.port { - ports.push({ number: port }); - } - - let var readiness: plus.Probe? = nil; - if let x = props.readiness { - if let port = props.port { - readiness = plus.Probe.fromHttpGet(x, port: port); - } else { - throw "Cannot setup readiness probe without a `port`"; - } - } - - let deployment = new plus.Deployment( - replicas: props.replicas, - metadata: { - name: props.name - }, - ); - - deployment.addContainer( - image: "\{\{ .Values.image }}", - envVariables: envVariables.copy(), - ports: ports.copy(), - readiness: readiness, - args: props.args, - securityContext: { - ensureNonRoot: false, - } - ); - - let isPublic = props.public ?? false; - let var serviceType: plus.ServiceType? = nil; - - if isPublic { - serviceType = plus.ServiceType.NODE_PORT; - } - - let service = deployment.exposeViaService( - name: props.name, - serviceType: serviceType, - ); - - if isPublic { - new plus.Ingress( - metadata: { - name: props.name, - annotations: { - "kubernetes.io/ingress.class": "alb", - "alb.ingress.kubernetes.io/scheme": "internet-facing", - "alb.ingress.kubernetes.io/target-type": "ip", - "alb.ingress.kubernetes.io/healthcheck-protocol": "HTTP", - "alb.ingress.kubernetes.io/healthcheck-port": "traffic-port", - "alb.ingress.kubernetes.io/healthcheck-path": "/", - } - }, - defaultBackend: plus.IngressBackend.fromService(service), - ); - } - - this.name = props.name; - } - - pub toHelm(): str { - let wingdir = std.Node.of(this).app.workdir; - return _Chart.toHelmChart(wingdir, this); - } - - extern "./helm.js" pub static toHelmChart(wingdir: str, chart: cdk8s.Chart): str; - } - - - let chart = new _Chart(props); + let chart = new helm.Chart(props); - let release = new helm.release.Release( + let release = new helm_provider.release.Release( provider: cluster.helmProvider(), dependsOn: deps.copy(), name: props.name, - chart: chart.toHelm(), + chart: chart.toHelm(nodeof(this).app.workdir), values: ["image: {image}"], ); diff --git a/containers/workload.w b/containers/workload.w index c6bd7d66..809a6b8c 100644 --- a/containers/workload.w +++ b/containers/workload.w @@ -2,8 +2,10 @@ bring util; bring "./workload.sim.w" as sim; bring "./workload.tfaws.w" as tfaws; bring "./api.w" as api; - +bring "./helm.w" as helm; bring http; +bring fs; +bring ui; pub class Workload { /** internal url, `nil` if there is no exposed port */ @@ -19,13 +21,50 @@ pub class Workload { let w = new sim.Workload_sim(props) as props.name; this.internalUrl = w.internalUrl; this.publicUrl = w.publicUrl; + nodeof(w).hidden = true; + + if let url = w.internalUrl { + new ui.ValueField("Internal URL", url) as "internal_url"; + } - } elif target == "tf-aws" { + if let url = w.publicUrl { + new ui.ValueField("Public URL", url) as "public_url"; + } + + return this; + } + + let provider = this.resolveProvider(target); + if provider == "eks" { let w = new tfaws.Workload_tfaws(props) as props.name; this.internalUrl = w.internalUrl; this.publicUrl = w.publicUrl; - } else { - throw "unsupported target {target}"; + return this; } + + if provider == "helm" { + let w = new helm.Chart(props); + this.internalUrl = "http://dummy"; + this.publicUrl = "http://dummy"; + w.toHelm(fs.join(nodeof(this).app.workdir, "..")); + return this; + } + + throw "unsupported provider {provider}"; + } + + resolveProvider(target: str): str { + let allowed = ["eks", "helm"]; + let params: Json = nodeof(this).app.parameters.value("containers"); + let provider = params?.tryGet("provider")?.tryAsStr(); + if provider == nil { + throw "Missing 'provider' under 'containers' in wing.toml. Allowed values are {allowed.join(", ")}"; + } + + if !allowed.contains(provider!) { + throw "Invalid provider {provider!}. Allowed values are {allowed.join(", ")}"; + } + + return provider!; } }