-
Notifications
You must be signed in to change notification settings - Fork 0
feat: builder #1
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
Merged
Merged
Changes from 1 commit
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
5232267
feat: builder
rswanson f522737
chore: readme
rswanson b6a5a39
fix: copilot review comments
rswanson d36376a
chore: go mod tidy
rswanson 4e2aa22
chore: .github dir contents
rswanson b4cbc16
feat: tests
rswanson 796fcf1
fix: run tests in ci
rswanson 5d64033
fix: action workflow name
rswanson 1af8e99
fix: construct json better
rswanson File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,245 @@ | ||
| // Package builder provides a Pulumi component for deploying a builder service to Kubernetes. | ||
| package builder | ||
|
|
||
| import ( | ||
| "encoding/json" | ||
| "fmt" | ||
|
|
||
| "github.com/pulumi/pulumi-aws/sdk/v6/go/aws/iam" | ||
| crd "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/apiextensions" | ||
| appsv1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/apps/v1" | ||
| corev1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/core/v1" | ||
| metav1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/meta/v1" | ||
| "github.com/pulumi/pulumi/sdk/v3/go/pulumi" | ||
| ) | ||
|
|
||
| // NewBuilder creates a new builder component with the given configuration. | ||
| func NewBuilder(ctx *pulumi.Context, args BuilderComponentArgs, opts ...pulumi.ResourceOption) (*BuilderComponent, error) { | ||
| if err := args.Validate(); err != nil { | ||
| return nil, fmt.Errorf("invalid builder component args: %w", err) | ||
| } | ||
|
|
||
| component := &BuilderComponent{ | ||
| BuilderComponentArgs: args, | ||
| } | ||
| err := ctx.RegisterComponentResource("the-builder:index:Builder", args.Name, component) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("failed to register component resource: %w", err) | ||
| } | ||
|
|
||
| // Create service account | ||
| sa, err := corev1.NewServiceAccount(ctx, "builder-sa", &corev1.ServiceAccountArgs{ | ||
| Metadata: &metav1.ObjectMetaArgs{ | ||
| Name: pulumi.String("builder-sa"), | ||
| Namespace: pulumi.String(args.Namespace), | ||
| }, | ||
| }, pulumi.Parent(component)) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("failed to create service account: %w", err) | ||
| } | ||
| component.ServiceAccount = sa | ||
|
|
||
| // Create IAM role | ||
| assumeRolePolicy := IAMPolicy{ | ||
| Version: "2012-10-17", | ||
| Statement: []IAMStatement{ | ||
| { | ||
| Sid: "AllowEksAuthToAssumeRoleForPodIdentity", | ||
| Effect: "Allow", | ||
| Principal: struct { | ||
| Service []string `json:"Service"` | ||
| }{ | ||
| Service: []string{ | ||
| "pods.eks.amazonaws.com", | ||
| "ec2.amazonaws.com", | ||
| }, | ||
| }, | ||
| Action: []string{ | ||
| "sts:AssumeRole", | ||
| "sts:TagSession", | ||
| }, | ||
| }, | ||
| }, | ||
| } | ||
|
|
||
| assumeRolePolicyJSON, err := json.Marshal(assumeRolePolicy) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("failed to marshal assume role policy: %w", err) | ||
| } | ||
|
|
||
| role, err := iam.NewRole(ctx, "builder-role", &iam.RoleArgs{ | ||
| AssumeRolePolicy: pulumi.String(assumeRolePolicyJSON), | ||
| Description: pulumi.String("Role for builder pod to assume"), | ||
| Tags: pulumi.StringMap{ | ||
| "Name": pulumi.String("builder-role"), | ||
| }, | ||
| }, pulumi.Parent(component)) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("failed to create IAM role: %w", err) | ||
| } | ||
| component.IAMRole = role | ||
|
|
||
| // Create KMS policy | ||
| policyJSON := createKMSPolicy(args.BuilderEnv.BuilderKey) | ||
|
|
||
| policy, err := iam.NewPolicy(ctx, "quinceyAppPolicy", &iam.PolicyArgs{ | ||
| Policy: policyJSON, | ||
| }, pulumi.Parent(component)) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("failed to create IAM policy: %w", err) | ||
| } | ||
| component.IAMPolicy = policy | ||
|
|
||
| // Attach policy to role | ||
| _, err = iam.NewRolePolicyAttachment(ctx, "builder-role-policy-attachment", &iam.RolePolicyAttachmentArgs{ | ||
| Role: role.Name, | ||
| PolicyArn: policy.Arn, | ||
| }, pulumi.Parent(component)) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("failed to attach policy to role: %w", err) | ||
| } | ||
|
|
||
| // Create deployment | ||
| deployment, err := appsv1.NewDeployment(ctx, "builder-deployment", &appsv1.DeploymentArgs{ | ||
| Metadata: &metav1.ObjectMetaArgs{ | ||
| Name: pulumi.String("builder-deployment"), | ||
| Namespace: pulumi.String(args.Namespace), | ||
| }, | ||
| Spec: &appsv1.DeploymentSpecArgs{ | ||
| Replicas: pulumi.Int(DefaultReplicas), | ||
| Selector: &metav1.LabelSelectorArgs{ | ||
| MatchLabels: args.AppLabels.Labels, | ||
| }, | ||
| Template: &corev1.PodTemplateSpecArgs{ | ||
| Metadata: &metav1.ObjectMetaArgs{ | ||
| Labels: args.AppLabels.Labels, | ||
| }, | ||
| Spec: &corev1.PodSpecArgs{ | ||
| ServiceAccountName: pulumi.String("builder-sa"), | ||
| Containers: corev1.ContainerArray{ | ||
| &corev1.ContainerArgs{ | ||
| Name: pulumi.String("builder"), | ||
| Image: pulumi.String(args.Image), | ||
| Env: createEnvVars(args.BuilderEnv), | ||
| Ports: corev1.ContainerPortArray{ | ||
| &corev1.ContainerPortArgs{ | ||
| ContainerPort: args.BuilderEnv.BuilderPort, | ||
| }, | ||
| &corev1.ContainerPortArgs{ | ||
| ContainerPort: pulumi.Int(MetricsPort), | ||
| }, | ||
| }, | ||
| Resources: &corev1.ResourceRequirementsArgs{ | ||
| Limits: pulumi.StringMap{ | ||
| "cpu": pulumi.String("2"), | ||
| "memory": pulumi.String("2Gi"), | ||
| }, | ||
| Requests: pulumi.StringMap{ | ||
| "cpu": pulumi.String("1"), | ||
| "memory": pulumi.String("1Gi"), | ||
| }, | ||
| }, | ||
| LivenessProbe: &corev1.ProbeArgs{ | ||
| HttpGet: &corev1.HTTPGetActionArgs{ | ||
| Path: pulumi.String("/healthcheck"), | ||
| Port: args.BuilderEnv.BuilderPort, | ||
| }, | ||
| InitialDelaySeconds: pulumi.Int(5), | ||
| PeriodSeconds: pulumi.Int(1), | ||
| TimeoutSeconds: pulumi.Int(1), | ||
| FailureThreshold: pulumi.Int(3), | ||
| }, | ||
| ReadinessProbe: &corev1.ProbeArgs{ | ||
| HttpGet: &corev1.HTTPGetActionArgs{ | ||
| Path: pulumi.String("/healthcheck"), | ||
| Port: args.BuilderEnv.BuilderPort, | ||
| }, | ||
| InitialDelaySeconds: pulumi.Int(5), | ||
| PeriodSeconds: pulumi.Int(10), | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| }, pulumi.DependsOn([]pulumi.Resource{role, policy}), pulumi.DeleteBeforeReplace(true), pulumi.Parent(component)) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("failed to create deployment: %w", err) | ||
| } | ||
| component.Deployment = deployment | ||
|
|
||
| // Create service | ||
| service, err := corev1.NewService(ctx, "builder-service", &corev1.ServiceArgs{ | ||
| Metadata: &metav1.ObjectMetaArgs{ | ||
| Name: pulumi.String("builder-service"), | ||
| Namespace: pulumi.String(args.Namespace), | ||
| Annotations: pulumi.StringMap{ | ||
| "prometheus.io/scrape": pulumi.String("true"), | ||
| "prometheus.io/port": pulumi.Sprintf("%d", MetricsPort), | ||
| "prometheus.io/path": pulumi.String("/metrics"), | ||
| }, | ||
| }, | ||
| Spec: &corev1.ServiceSpecArgs{ | ||
| Selector: args.AppLabels.Labels, | ||
| Ports: corev1.ServicePortArray{ | ||
| &corev1.ServicePortArgs{ | ||
| Port: args.BuilderEnv.BuilderPort, | ||
| TargetPort: args.BuilderEnv.BuilderPort, | ||
| Name: pulumi.String("http"), | ||
| }, | ||
| &corev1.ServicePortArgs{ | ||
| Port: pulumi.Int(MetricsPort), | ||
| TargetPort: pulumi.Int(MetricsPort), | ||
| Name: pulumi.String("metrics"), | ||
| }, | ||
| }, | ||
| }, | ||
| }, pulumi.DependsOn([]pulumi.Resource{deployment}), pulumi.DeleteBeforeReplace(true), pulumi.Parent(component)) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("failed to create service: %w", err) | ||
| } | ||
| component.Service = service | ||
|
|
||
| // Create pod monitor | ||
| _, err = crd.NewCustomResource(ctx, "builder-svcmon", &crd.CustomResourceArgs{ | ||
| ApiVersion: pulumi.String("monitoring.coreos.com/v1"), | ||
| Kind: pulumi.String("PodMonitor"), | ||
| Metadata: &metav1.ObjectMetaArgs{ | ||
| Name: pulumi.String("builder-pod-monitor"), | ||
| Namespace: pulumi.String(args.Namespace), | ||
| }, | ||
| OtherFields: map[string]interface{}{ | ||
| "spec": map[string]interface{}{ | ||
| "selector": map[string]interface{}{ | ||
| "matchLabels": args.AppLabels.Labels, | ||
| }, | ||
| "namespaceSelector": map[string]interface{}{ | ||
| "any": true, | ||
| }, | ||
| "podMetricsEndpoints": []map[string]interface{}{ | ||
| { | ||
| "port": "metrics", | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| }, pulumi.Parent(component)) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("failed to create pod monitor: %w", err) | ||
| } | ||
|
|
||
| return component, nil | ||
| } | ||
|
|
||
| // GetServiceURL returns the URL of the builder service | ||
| func (c *BuilderComponent) GetServiceURL() pulumi.StringOutput { | ||
| return pulumi.Sprintf("http://%s.%s.svc.cluster.local", c.Service.Metadata.Name(), c.Service.Metadata.Namespace()) | ||
| } | ||
|
|
||
| // GetMetricsURL returns the URL of the builder metrics endpoint | ||
| func (c *BuilderComponent) GetMetricsURL() pulumi.StringOutput { | ||
| return pulumi.Sprintf("http://%s.%s.svc.cluster.local:%d/metrics", | ||
| c.Service.Metadata.Name(), | ||
| c.Service.Metadata.Namespace(), | ||
| MetricsPort) | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| package builder | ||
|
|
||
| import ( | ||
| "reflect" | ||
| "strings" | ||
| "unicode" | ||
|
|
||
| corev1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/core/v1" | ||
| "github.com/pulumi/pulumi/sdk/v3/go/pulumi" | ||
| ) | ||
|
|
||
| // createKMSPolicy creates a KMS policy for the builder service. | ||
| func createKMSPolicy(key pulumi.StringInput) pulumi.StringOutput { | ||
| return pulumi.Sprintf(`{ | ||
| "Version": "2012-10-17", | ||
| "Statement": [ | ||
| { | ||
| "Effect": "Allow", | ||
| "Action": [ | ||
| "kms:Sign", | ||
| "kms:GetPublicKey" | ||
| ], | ||
| "Resource": %s | ||
| } | ||
| ] | ||
| }`, key) | ||
| } | ||
|
|
||
| // createEnvVars creates environment variables by automatically mapping | ||
| // struct field names to environment variable names. | ||
| func createEnvVars(env BuilderEnv) corev1.EnvVarArray { | ||
| result := corev1.EnvVarArray{} | ||
|
|
||
| // Special case for BuilderPort as it needs string conversion | ||
| result = append(result, &corev1.EnvVarArgs{ | ||
| Name: pulumi.String("BUILDER_PORT"), | ||
| Value: pulumi.Sprintf("%d", env.BuilderPort), | ||
rswanson marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| }) | ||
|
|
||
| // Process all string inputs from the struct's tags | ||
| envVarMap := getEnvironmentVarsFromStruct(env) | ||
| for name, value := range envVarMap { | ||
| if name != "BUILDER_PORT" { // Skip the one we already handled | ||
| result = append(result, &corev1.EnvVarArgs{ | ||
| Name: pulumi.String(name), | ||
| Value: value.(pulumi.StringInput), | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| return result | ||
| } | ||
|
|
||
| // getEnvironmentVarsFromStruct uses reflection to extract environment variables from struct tags | ||
| func getEnvironmentVarsFromStruct(env BuilderEnv) map[string]pulumi.Input { | ||
| result := make(map[string]pulumi.Input) | ||
|
|
||
| t := reflect.TypeOf(env) | ||
| v := reflect.ValueOf(env) | ||
|
|
||
| for i := 0; i < t.NumField(); i++ { | ||
| field := t.Field(i) | ||
|
|
||
| // Get the field value | ||
| fieldValue := v.Field(i).Interface() | ||
|
|
||
| // Skip nil values and BuilderPort (handled specially) | ||
| if fieldValue == nil || field.Name == "BuilderPort" { | ||
| continue | ||
| } | ||
|
|
||
| // Convert camelCase to SNAKE_CASE for env var name | ||
| envName := camelToSnake(field.Name) | ||
|
|
||
| // Add to map | ||
| result[envName] = fieldValue.(pulumi.Input) | ||
| } | ||
|
|
||
| return result | ||
| } | ||
|
|
||
| // camelToSnake converts a camelCase string to SNAKE_CASE | ||
| func camelToSnake(s string) string { | ||
| var result strings.Builder | ||
| for i, r := range s { | ||
| if unicode.IsUpper(r) { | ||
| if i > 0 { | ||
| result.WriteRune('_') | ||
| } | ||
| result.WriteRune(unicode.ToUpper(r)) | ||
| } else { | ||
| result.WriteRune(unicode.ToUpper(r)) | ||
| } | ||
| } | ||
| return result.String() | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.