Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add AWS EKS example #358

Merged
merged 4 commits into from
Feb 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions core/src/main/scala/besom/internal/Resource.scala
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ sealed trait Resource:
case _: CustomResource => true
case _ => false

private[internal] def asString: Result[Option[String]] = urn.getValue.map(_.map(v => s"${this.getClass.getSimpleName}($v)"))

trait CustomResource extends Resource:
/** @return
* the [[ResourceId]] of the resource
Expand Down
10 changes: 5 additions & 5 deletions core/src/main/scala/besom/internal/ResourceDecoder.scala
Original file line number Diff line number Diff line change
Expand Up @@ -34,20 +34,20 @@ object ResourceDecoder:
.decode(value, propertyLabel)
.map(_.withDependency(resource))
.tapBoth(
err => log.debug(s"failed to extract custom property $propertyName from $value: $err"),
value => log.debug(s"extracted custom property $propertyName from $value")
err => log.debug(s"failed to extract custom property '$propertyName' from '$value': '$err'"),
value => log.debug(s"extracted custom property '$propertyName' from '$value'")
)

ValidatedResult.transparent(decoded).in { result =>
log.debug(s"extracting custom property $propertyName from $value using decoder $decoder") *> result
log.debug(s"extracting custom property '$propertyName' from '$value' using decoder '$decoder'") *> result
}
}
.getOrElse {
if ctx.isDryRun then ValidatedResult.valid(OutputData.unknown().withDependency(resource))
// TODO: formatted DecodingError
else
ValidatedResult.invalid(
DecodingError(s"Missing property $propertyName in resource $resourceLabel", label = propertyLabel)
DecodingError(s"Missing property '$propertyName' in resource '$resourceLabel'", label = propertyLabel)
)
}
.map(_.withDependencies(fieldDependencies))
Expand Down Expand Up @@ -86,7 +86,7 @@ object ResourceDecoder:
errorOrResourceResult match
case Left(err) =>
val message =
s"Resolve resource: received error from gRPC call: ${err.getMessage()}, failing resolution"
s"Resolve resource: received error from gRPC call: '${err.getMessage}', failing resolution"

ctx.logger.error(message) *> failAllPromises(err)

Expand Down
92 changes: 52 additions & 40 deletions core/src/main/scala/besom/internal/resources.scala
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package besom.internal

import scala.annotation.unused

//noinspection ScalaFileName
class Resources private (
private val resources: Ref[Map[Resource, ResourceState]],
private val cache: Ref[Map[(String, String), Promise[Resource]]]
Expand All @@ -25,57 +28,65 @@ class Resources private (
add(rc, rcs)
case (compb: ComponentBase, comprs: ComponentResourceState) =>
add(compb, comprs)
case _ => Result.fail(new Exception(s"resource ${resource} and state ${state} don't match"))
case _ =>
resource.asString.flatMap(s => Result.fail(Exception(s"resource ${s} and state ${state} don't match")))

def getStateFor(resource: ProviderResource): Result[ProviderResourceState] =
resources.get.flatMap(_.get(resource) match
case Some(state) =>
state match
case crs: CustomResourceState =>
Result.fail(new Exception(s"state for ProviderResource ${resource} is a CustomResourceState!"))
case prs: ProviderResourceState => Result.pure(prs)
case comprs: ComponentResourceState =>
Result.fail(new Exception(s"state for ProviderResource ${resource} is a ComponentResourceState!"))

case None => Result.fail(new Exception(s"state for resource ${resource} not found"))
)
resources.get.flatMap {
_.get(resource) match
case Some(state) =>
state match
case _: CustomResourceState =>
resource.asString.flatMap(s => Result.fail(Exception(s"state for ProviderResource ${s} is a CustomResourceState!")))
case prs: ProviderResourceState => Result.pure(prs)
case _: ComponentResourceState =>
resource.asString.flatMap(s => Result.fail(Exception(s"state for ProviderResource ${s} is a ComponentResourceState!")))

case None =>
resource.asString.flatMap(s => Result.fail(Exception(s"state for resource ${s} not found")))
}

def getStateFor(resource: CustomResource): Result[CustomResourceState] =
resources.get.flatMap(_.get(resource) match
case Some(state) =>
state match
case crs: CustomResourceState => Result.pure(crs)
case prs: ProviderResourceState =>
Result.fail(new Exception(s"state for CustomResource ${resource} is a ProviderResourceState!"))
case comprs: ComponentResourceState =>
Result.fail(new Exception(s"state for CustomResource ${resource} is a ComponentResourceState!"))

case None => Result.fail(new Exception(s"state for resource ${resource} not found"))
)
resources.get.flatMap {
_.get(resource) match
case Some(state) =>
state match
case crs: CustomResourceState => Result.pure(crs)
case _: ProviderResourceState =>
resource.asString.flatMap(s => Result.fail(Exception(s"state for CustomResource ${s} is a ProviderResourceState!")))
case _: ComponentResourceState =>
resource.asString.flatMap(s => Result.fail(Exception(s"state for CustomResource ${s} is a ComponentResourceState!")))

case None =>
resource.asString.flatMap(s => Result.fail(Exception(s"state for resource ${s} not found")))
}

def getStateFor(resource: ComponentResource): Result[ComponentResourceState] =
resources.get.flatMap(_.get(resource) match
case Some(state) =>
state match
case crs: CustomResourceState =>
Result.fail(new Exception(s"state for ComponentResource ${resource} is a CustomResourceState!"))
case prs: ProviderResourceState =>
Result.fail(new Exception(s"state for ComponentResource ${resource} is a ProviderResourceState!"))
case comprs: ComponentResourceState => Result.pure(comprs)

case None => Result.fail(new Exception(s"state for resource ${resource} not found"))
)
resources.get.flatMap {
_.get(resource) match
case Some(state) =>
state match
case _: CustomResourceState =>
resource.asString.flatMap(s => Result.fail(Exception(s"state for ComponentResource ${s} is a CustomResourceState!")))
case _: ProviderResourceState =>
resource.asString.flatMap(s => Result.fail(Exception(s"state for ComponentResource ${s} is a ProviderResourceState!")))
case comprs: ComponentResourceState => Result.pure(comprs)

case None =>
resource.asString.flatMap(s => Result.fail(Exception(s"state for resource ${s} not found")))
}

def getStateFor(resource: Resource): Result[ResourceState] =
resources.get.flatMap(_.get(resource) match
case Some(state) => Result.pure(state)
case None => Result.fail(new Exception(s"state for resource ${resource} not found"))
)
resources.get.flatMap {
_.get(resource) match
case Some(state) => Result.pure(state)
case None => resource.asString.flatMap(s => Result.fail(Exception(s"state for resource ${s} not found")))
}

def updateStateFor(resource: Resource)(f: ResourceState => ResourceState): Result[Unit] =
resources.update(_.updatedWith(resource)(_.map(f)))

def cacheResource(typ: String, name: String, args: Any, opts: ResourceOptions, resource: Resource): Result[Boolean] =
def cacheResource(typ: String, name: String, @unused args: Any, @unused opts: ResourceOptions, resource: Resource): Result[Boolean] =
cache.get.flatMap(_.get((typ, name)) match
case Some(_) => Result.pure(false)
case None =>
Expand All @@ -86,11 +97,12 @@ class Resources private (
}
)

def getCachedResource(typ: String, name: String, args: Any, opts: ResourceOptions): Result[Resource] =
def getCachedResource(typ: String, name: String, @unused args: Any, @unused opts: ResourceOptions): Result[Resource] =
cache.get.flatMap(_((typ, name)).get)

end Resources

//noinspection ScalaFileName
object Resources:
def apply(): Result[Resources] =
for
Expand Down
10 changes: 10 additions & 0 deletions examples/aws-eks/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
### Scala an JVM
*.class
*.log
.bsp
.scala-build

# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*

kubeconfig.json
60 changes: 60 additions & 0 deletions examples/aws-eks/Main.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import besom.*
import besom.api.aws
import besom.api.eks
import besom.api.kubernetes as k8s

@main def main = Pulumi.run {
// Get the default VPC and select the default subnet
val vpc = aws.ec2.getVpc(aws.ec2.GetVpcArgs(default = true))
val subnet = vpc.flatMap(vpc =>
aws.ec2.getSubnet(
aws.ec2.GetSubnetArgs(
vpcId = vpc.id,
defaultForAz = true
)
)
)

// Create an EKS cluster using the default VPC and subnet
val cluster = eks.Cluster(
"my-cluster",
eks.ClusterArgs(
vpcId = vpc.id,
subnetIds = List(subnet.id),
instanceType = "t2.medium",
desiredCapacity = 2,
minSize = 1,
maxSize = 3,
storageClasses = "gp2"
)
)

val k8sProvider = k8s.Provider(
"k8s-provider",
k8s.ProviderArgs(
kubeconfig = cluster.kubeconfigJson
)
)

val pod = k8s.core.v1.Pod(
"mypod",
k8s.core.v1.PodArgs(
spec = k8s.core.v1.inputs.PodSpecArgs(
containers = List(
k8s.core.v1.inputs.ContainerArgs(
name = "echo",
image = "k8s.gcr.io/echoserver:1.4"
)
)
)
),
opts = opts(
provider = k8sProvider,
dependsOn = cluster,
deletedWith = cluster // skip deletion to save time, since it will be deleted with the cluster
)
)

// Export the cluster's kubeconfig
Stack(cluster, pod).exports(kubeconfig = cluster.kubeconfig)
}
8 changes: 8 additions & 0 deletions examples/aws-eks/Pulumi.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
name: aws-eks
description: EKS cluster example
runtime: scala
template:
config:
aws:region:
description: The AWS region to deploy into
default: us-west-2
57 changes: 57 additions & 0 deletions examples/aws-eks/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Amazon EKS Cluster

This example deploys an EKS Kubernetes cluster with an EBS-backed StorageClass.

## Prerequisites

[Follow the instructions](https://www.pulumi.com/docs/clouds/aws/get-started/begin/)
to get started with Pulumi & AWS.

## Deploying

Note: some values in this example will be different from run to run.
These values are indicated with `***`.

1. Create a new stack, which is an isolated deployment target for this example:

```bash
pulumi stack init aws-eks-dev
```

2. Set the AWS region:

```bash
pulumi config set aws:region us-west-2
```

We recommend using `us-west-2` to host your EKS cluster as other regions (notably `us-east-1`) may have capacity issues that prevent EKS
clusters from creating.

We are tracking enabling the creation of VPCs limited to specific AZs to unblock this in `us-east-1`: pulumi/pulumi-awsx#32

3. Stand up the EKS cluster:

```bash
pulumi up
```
4. After 10-15 minutes, your cluster will be ready, and the `kubeconfig` JSON you'll use to connect to the cluster will
be available as an output. You can save this `kubeconfig` to a file like so:

```bash
pulumi stack output kubeconfig --show-secrets > kubeconfig.json
```

Once you have this file in hand, you can interact with your new cluster as usual via `kubectl`:

```bash
kubectl --kubeconfig=./kubeconfig.json get pods --all-namespaces
```

5. To clean up resources, destroy your stack and remove it:

```bash
pulumi destroy
```
```bash
pulumi stack rm aws-eks
```
6 changes: 6 additions & 0 deletions examples/aws-eks/project.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
//> using scala "3.3.1"
//> using options -Werror -Wunused:all -Wvalue-discard -Wnonunit-statement
//> using plugin "org.virtuslab::besom-compiler-plugin:0.2.0-SNAPSHOT"
//> using dep "org.virtuslab::besom-core:0.2.0-SNAPSHOT"
//> using dep "org.virtuslab::besom-eks:2.2.1-core.0.2-SNAPSHOT"
//> using repository sonatype:snapshots
45 changes: 26 additions & 19 deletions scripts/Packages.scala
Original file line number Diff line number Diff line change
Expand Up @@ -125,14 +125,6 @@ object Packages:
generatedFile
}

def generateSelected(targetPath: os.Path, packages: List[String]): os.Path = {
val selectedPackages = readPackagesMetadata(targetPath, selected = packages)
.filter(p => packages.contains(p.name))
val metadata = generate(selectedPackages)
os.write.over(generatedFile, PackageMetadata.toJson(metadata), createFolders = true)
generatedFile
}

def publishLocalAll(sourceFile: os.Path): os.Path = {
val generated = PackageMetadata
.fromJsonList(os.read(sourceFile: os.Path))
Expand All @@ -142,15 +134,6 @@ object Packages:
publishedLocalFile
}

def publishLocalSelected(sourceFile: os.Path, packages: List[String]): os.Path = {
val generated = PackageMetadata
.fromJsonList(os.read(sourceFile))
.filter(p => packages.contains(p.name))
val published = publishLocal(generated)
os.write.over(publishedLocalFile, PackageMetadata.toJson(published), createFolders = true)
publishedLocalFile
}

def publishMavenAll(sourceFile: os.Path): os.Path = {
val generated = PackageMetadata
.fromJsonList(os.read(sourceFile: os.Path))
Expand All @@ -160,11 +143,35 @@ object Packages:
publishedMavenFile
}

def generateSelected(targetPath: os.Path, packages: List[String]): os.Path = {
val readPackages = readPackagesMetadata(targetPath, selected = packages)
val selectedPackages = packages.map { p =>
readPackages.find(_.name == p).getOrElse(throw Exception(s"Package '$p' not found"))
}.toVector

val metadata = generate(selectedPackages)
os.write.over(generatedFile, PackageMetadata.toJson(metadata), createFolders = true)
generatedFile
}

def publishLocalSelected(sourceFile: os.Path, packages: List[String]): os.Path = {
val generated = PackageMetadata
.fromJsonList(os.read(sourceFile))
val selectedPackages = packages.map { p =>
generated.find(_.name == p).getOrElse(throw Exception(s"Package '$p' not found"))
}.toVector
val published = publishLocal(selectedPackages)
os.write.over(publishedLocalFile, PackageMetadata.toJson(published), createFolders = true)
publishedLocalFile
}

def publishMavenSelected(sourceFile: os.Path, packages: List[String]): os.Path = {
val generated = PackageMetadata
.fromJsonList(os.read(sourceFile))
.filter(p => packages.contains(p.name))
val published = publishMaven(generated)
val selectedPackages = packages.map { p =>
generated.find(_.name == p).getOrElse(throw Exception(s"Package '$p' not found"))
}.toVector
val published = publishMaven(selectedPackages)
os.write.over(publishedMavenFile, PackageMetadata.toJson(published), createFolders = true)
publishedMavenFile
}
Expand Down
Loading
Loading