Skip to content

Commit

Permalink
Add scalar types for most commonly used resource outputs (#1445)
Browse files Browse the repository at this point in the history
To ease the impact of the breaking API changes caused by generating the
node SDK, we decided to add additional scalar inputs that simplify UX
across all SDKs (for more details [see internal
doc](https://docs.google.com/document/d/1f97nmDUG_nrZSllYxu_XSeI7ON8vhZzfVrdBTQQmZzw/edit#heading=h.fbweiu8gc5bw)).

This change adds the scalar properties mentioned in the doc and adds
acceptance tests for them.
While adding the acceptance tests I noticed that running pods on Fargate
doesn't work deterministically. In some cases the cluster fails to get
healthy (coredns stuck in pending).
This was caused by a race-condition between coredns starting and the
fargate profile being created. If the fargate profile deployed after
coredns, the pods got stuck in pending because they got assigned to the
`default-scheduler` instead of the `fargate-scheduler`.
The fix is relatively easy; making coredns depend on the fargate
profile.

I'll separately update the migration guide.

### New properties

| Existing Resource |  | New Top Level Property | Description |
| :---- | :---- | :---- | :---- |
| `clusterSecurityGroup: Output<aws.ec2.SecurityGroup \| undefined>` | |
`clusterSecurityGroupId: Output<string>` | Only really useful property
of a security group. Used to add additional ingress/egress rules.
Default to `the EKS created security group id` |
| `nodeSecurityGroup: Output<aws.ec2.SecurityGroup \| undefined>` | |
`nodeSecurityGroupId: Output<string>` | |
| `eksClusterIngressRule: Output<aws.ec2.SecurityGroupRule \|
undefined>` | | `clusterIngressRuleId: Output<string>` | Only really
useful property of a rule. Default to `””` |
| `defaultNodeGroup: Output<eks.NodeGroupData \| undefined>` | |
`defaultNodeGroupAsgName: Output<string>` | The only useful property of
the default node group is the auto scaling group. Exposing its name
allows users to reference it in IAM roles, tags, etc. Default to `””` |
| `core` | `fargateProfile: Output<aws.eks.FargateProfile \| undefined>`
| `fargateProfileId: Output<string>` | The id of the fargate profile.
Can be used to reference it. Default to `””` |
| | | `fargateProfileStatus: Output<string>` | The status of the fargate
profile. Default to `””` |
| | `oidcProvider: Output<aws.iam.OpenIdConnectProvider \| undefined>` |
`oidcProviderArn: Output<string>` & `oidcProviderUrl: Output<string>` &
`oidcIssuer: Output<string` | Arn and Url are properties needed to set
up IAM identities for pods (required for the assume role policy of the
IAM role). Users currently need to trim the `https://` part of the url
to actually use it. We should expose `oidcProvider` with that already
done to ease usage. |


Fixes #1041
  • Loading branch information
flostadler authored Oct 16, 2024
1 parent 4f30a8d commit 1e2e4aa
Show file tree
Hide file tree
Showing 23 changed files with 892 additions and 179 deletions.
2 changes: 1 addition & 1 deletion examples/cluster/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const cluster1 = new eks.Cluster(`${projectName}-1`, {
nodeAmiId: "ami-066e69f6f03b5383e",
});

export const defaultAsgArn: pulumi.Output<string> = cluster1.defaultNodeGroup.apply(ng => ng?.autoScalingGroup.arn ?? pulumi.output(""));
export const defaultAsgName: pulumi.Output<string> = cluster1.defaultNodeGroupAsgName;

const cluster2 = new eks.Cluster(`${projectName}-2`, {
vpcId: vpc.vpcId,
Expand Down
112 changes: 111 additions & 1 deletion examples/examples_nodejs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"os/exec"
"path"
"path/filepath"
"strings"
"testing"
"time"

Expand Down Expand Up @@ -56,7 +57,7 @@ func TestAccCluster(t *testing.T) {
info.Outputs["kubeconfig4"],
)

assert.NotEmpty(t, info.Outputs["defaultAsgArn"], "should have a default ASG")
assert.NotEmpty(t, info.Outputs["defaultAsgName"], "should have a default ASG")

// let's test there's a iamRoleArn specified for the cluster
assert.NotEmpty(t, info.Outputs["iamRoleArn"])
Expand Down Expand Up @@ -1061,3 +1062,112 @@ func TestAccPodSecurityGroups(t *testing.T) {

programTestWithExtraOptions(t, &test, nil)
}

func TestAccScalarTypes(t *testing.T) {
if testing.Short() {
t.Skip("skipping test in short mode.")
}
test := getJSBaseOptions(t).
With(integration.ProgramTestOptions{
Dir: path.Join(getCwd(t), "tests", "scalar-types"),
ExtraRuntimeValidation: func(t *testing.T, info integration.RuntimeValidationStackInfo) {
utils.RunEKSSmokeTest(t,
info.Deployment.Resources,
info.Outputs["kubeconfig1"],
info.Outputs["kubeconfig2"],
)

// cluster1 runs the default settings with a default node group and an oidc provider
require.NotNil(t, info.Outputs["cluster1"])
cluster1 := info.Outputs["cluster1"].(map[string]interface{})
require.NotNil(t, cluster1["eksCluster"])
eksCluster1 := cluster1["eksCluster"].(map[string]interface{})

require.NotNil(t, cluster1["clusterSecurityGroup"])
require.NotNil(t, cluster1["nodeSecurityGroup"])
assert.Equal(t, cluster1["clusterSecurityGroup"].(map[string]interface{})["id"], cluster1["clusterSecurityGroupId"].(string))
assert.Equal(t, cluster1["nodeSecurityGroup"].(map[string]interface{})["id"], cluster1["nodeSecurityGroupId"].(string))
assert.NotEmpty(t, cluster1["clusterIngressRuleId"])
assert.Empty(t, cluster1["fargateProfileId"])
assert.Empty(t, cluster1["fargateProfileStatus"])

require.NotNil(t, cluster1["defaultNodeGroup"])
defaultNodeGroup1 := cluster1["defaultNodeGroup"].(map[string]interface{})
assert.Equal(t, defaultNodeGroup1["autoScalingGroup"].(map[string]interface{})["name"], cluster1["defaultNodeGroupAsgName"].(string))

require.NotNil(t, cluster1["core"])
coreData1 := cluster1["core"].(map[string]interface{})
require.NotNil(t, coreData1["oidcProvider"])
oidcProvider1 := coreData1["oidcProvider"].(map[string]interface{})

assert.Equal(t, oidcProvider1["arn"], cluster1["oidcProviderArn"])
oidcProviderUrl1 := getOidcProviderUrl(t, eksCluster1)
assert.Equal(t, oidcProviderUrl1, cluster1["oidcProviderUrl"])
assert.Equal(t, strings.ReplaceAll(oidcProviderUrl1, "https://", ""), cluster1["oidcIssuer"],
"expected oidcIssuer to be the same as the oidcProvider url without the https:// prefix")

// cluster2 runs with fargate, no default node group, no default security groups and no oidc provider
require.NotNil(t, info.Outputs["cluster2"])
cluster2 := info.Outputs["cluster2"].(map[string]interface{})
require.NotNil(t, cluster2["eksCluster"])
eksCluster2 := cluster2["eksCluster"].(map[string]interface{})
require.NotNil(t, eksCluster2["vpcConfig"])
vpcConfig := eksCluster2["vpcConfig"].(map[string]interface{})

// AWS EKS always creates a security group for the cluster
eksSecurityGroupId := vpcConfig["clusterSecurityGroupId"]
require.NotEmpty(t, eksSecurityGroupId)

// verify that the cluster and node security group ID are set to the eks security group ID
assert.Equal(t, eksSecurityGroupId, cluster2["clusterSecurityGroupId"])
assert.Equal(t, eksSecurityGroupId, cluster2["nodeSecurityGroupId"])

// verify that the provider creates no security groups
assert.Nil(t, cluster2["clusterSecurityGroup"])
assert.Nil(t, cluster2["nodeSecurityGroup"])
assert.Empty(t, cluster2["clusterIngressRuleId"])

// verify that the provider creates no default node group
assert.Nil(t, cluster2["defaultNodeGroup"])
assert.Empty(t, cluster2["defaultNodeGroupAsgName"])

require.NotNil(t, cluster2["core"])
coreData2 := cluster2["core"].(map[string]interface{})

// verify that the provider creates no IAM OIDC provider
assert.Empty(t, cluster2["oidcProviderArn"])
assert.Nil(t, coreData2["oidcProvider"])

// every EKS cluster has an OIDC provider URL, even if no OIDC provider is created
oidcProviderUrl2 := cluster2["oidcProviderUrl"].(string)
assert.NotEmpty(t, oidcProviderUrl2)
assert.NotEmpty(t, cluster2["oidcIssuer"])
assert.Equal(t, strings.ReplaceAll(oidcProviderUrl2, "https://", ""), cluster2["oidcIssuer"],
"expected oidcIssuer to be the same as the oidcProvider url without the https:// prefix")

// verify that the provider creates a fargate profile
require.NotNil(t, coreData2["fargateProfile"])
fargateProfile := coreData2["fargateProfile"].(map[string]interface{})
assert.Equal(t, fargateProfile["id"], cluster2["fargateProfileId"])
assert.Equal(t, fargateProfile["status"], cluster2["fargateProfileStatus"])
},
})

programTestWithExtraOptions(t, &test, nil)
}

func getOidcProviderUrl(t *testing.T, eksCluster map[string]interface{}) string {
require.NotEmpty(t, eksCluster["identities"])
identities := eksCluster["identities"].([]interface{})
require.NotEmpty(t, identities[0])

require.Contains(t, identities[0].(map[string]interface{}), "oidcs")
require.NotEmpty(t, identities[0].(map[string]interface{})["oidcs"])
oidcs := identities[0].(map[string]interface{})["oidcs"].([]interface{})

require.NotEmpty(t, oidcs[0])
require.Contains(t, oidcs[0].(map[string]interface{}), "issuer")
require.NotEmpty(t, oidcs[0].(map[string]interface{})["issuer"])

return oidcs[0].(map[string]interface{})["issuer"].(string)
}
2 changes: 1 addition & 1 deletion examples/extra-sg/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ const nodeIngressRule = new aws.ec2.SecurityGroupRule("nodeIngressRule", {
fromPort: 0,
toPort: 65535,
protocol: "tcp",
securityGroupId: cluster.nodeSecurityGroup.apply((sg) => sg!.id),
securityGroupId: cluster.nodeSecurityGroupId,
sourceSecurityGroupId: customSecurityGroup.id,
});

Expand Down
13 changes: 5 additions & 8 deletions examples/oidc-iam-sa/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@ export const kubeconfig = cluster.kubeconfig;
if (!cluster?.core?.oidcProvider) {
throw new Error("Invalid cluster OIDC provider URL");
}
const clusterOidcProvider = cluster.core.oidcProvider;
export const clusterOidcProviderUrl = clusterOidcProvider.apply(u => u!.url);

// Setup Pulumi Kubernetes provider.
const provider = new k8s.Provider("eks-k8s", {
Expand All @@ -34,22 +32,21 @@ export const appsNamespaceName = appsNamespace.metadata.name;
// Create the new IAM policy for the Service Account using the
// AssumeRoleWebWebIdentity action.
const saName = "s3";
const oidcProviderArn = clusterOidcProvider.apply(o => o!.arn);
const saAssumeRolePolicy = pulumi.all([clusterOidcProviderUrl, oidcProviderArn, appsNamespaceName]).apply(([url, arn, namespace]) => aws.iam.getPolicyDocument({
const saAssumeRolePolicy = aws.iam.getPolicyDocumentOutput({
statements: [{
actions: ["sts:AssumeRoleWithWebIdentity"],
conditions: [{
test: "StringEquals",
values: [`system:serviceaccount:${namespace}:${saName}`],
variable: `${url.replace("https://", "")}:sub`,
values: [pulumi.interpolate`system:serviceaccount:${appsNamespaceName}:${saName}`],
variable: pulumi.interpolate`${cluster.oidcIssuer}:sub`,
}],
effect: "Allow",
principals: [{
identifiers: [arn],
identifiers: [cluster.oidcProviderArn],
type: "Federated",
}],
}],
}));
});

const saRole = new aws.iam.Role(saName, {
assumeRolePolicy: saAssumeRolePolicy.json,
Expand Down
3 changes: 3 additions & 0 deletions examples/tests/scalar-types/Pulumi.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
name: scalar-types
description: Tests retrieving the scalar properties of EKS clusters
runtime: nodejs
28 changes: 28 additions & 0 deletions examples/tests/scalar-types/iam.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import * as aws from "@pulumi/aws";
import * as pulumi from "@pulumi/pulumi";

const managedPolicyArns: string[] = [
"arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy",
"arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy",
"arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly",
"arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore",
];

// Creates a role and attches the EKS worker node IAM managed policies
export function createRole(name: string): aws.iam.Role {
const role = new aws.iam.Role(name, {
assumeRolePolicy: aws.iam.assumeRolePolicyForPrincipal({
Service: "ec2.amazonaws.com",
}),
});

let counter = 0;
for (const policy of managedPolicyArns) {
// Create RolePolicyAttachment without returning it.
const rpa = new aws.iam.RolePolicyAttachment(`${name}-policy-${counter++}`,
{ policyArn: policy, role: role },
);
}

return role;
}
37 changes: 37 additions & 0 deletions examples/tests/scalar-types/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@

import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
import * as awsx from "@pulumi/awsx";
import * as eks from "@pulumi/eks";
import * as iam from "./iam";

const role1 = iam.createRole("scalar-types-1");
const role2 = iam.createRole("scalar-types-2");

const eksVpc = new awsx.ec2.Vpc("scalar-types", {
enableDnsHostnames: true,
cidrBlock: "10.0.0.0/16",
});

export const cluster1 = new eks.Cluster("scalar-types-1", {
vpcId: eksVpc.vpcId,
authenticationMode: eks.AuthenticationMode.Api,
publicSubnetIds: eksVpc.publicSubnetIds,
privateSubnetIds: eksVpc.privateSubnetIds,
createOidcProvider: true,
});

export const kubeconfig1 = cluster1.kubeconfig;

export const cluster2 = new eks.Cluster("scalar-types-2", {
vpcId: eksVpc.vpcId,
authenticationMode: eks.AuthenticationMode.Api,
fargate: {
selectors: [{ namespace: "kube-system" }],
},
skipDefaultSecurityGroups: true,
publicSubnetIds: eksVpc.publicSubnetIds,
privateSubnetIds: eksVpc.privateSubnetIds,
});

export const kubeconfig2 = cluster2.kubeconfig;
13 changes: 13 additions & 0 deletions examples/tests/scalar-types/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "scalar-types",
"devDependencies": {
"@types/node": "latest",
"typescript": "^4.0.0"
},
"dependencies": {
"@pulumi/awsx": "^2.0.0",
"@pulumi/aws": "^6.50.1",
"@pulumi/eks": "latest",
"@pulumi/pulumi": "^3.0.0"
}
}
24 changes: 24 additions & 0 deletions examples/tests/scalar-types/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"compilerOptions": {
"outDir": "bin",
"target": "es6",
"lib": [
"es6"
],
"module": "commonjs",
"moduleResolution": "node",
"declaration": true,
"sourceMap": true,
"stripInternal": true,
"experimentalDecorators": true,
"pretty": true,
"noFallthroughCasesInSwitch": true,
"noImplicitAny": true,
"noImplicitReturns": true,
"forceConsistentCasingInFileNames": true,
"strictNullChecks": true
},
"files": [
"index.ts"
]
}
Loading

0 comments on commit 1e2e4aa

Please sign in to comment.