Skip to content
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
16 changes: 16 additions & 0 deletions charts/operator/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,22 @@ spec:
value: {{ .Values.config.region | quote }}
- name: AGENTS_SSM_PATH_PREFIX
value: {{ .Values.config.ssmPathPrefix | quote }}
{{- with .Values.config.tags.costCenter }}
- name: AGENTS_COST_CENTER
value: {{ . | quote }}
{{- end }}
{{- with .Values.config.tags.businessUnit }}
- name: AGENTS_BUSINESS_UNIT
value: {{ . | quote }}
{{- end }}
{{- with .Values.config.tags.dataClassification }}
- name: AGENTS_DATA_CLASSIFICATION
value: {{ . | quote }}
{{- end }}
{{- with .Values.config.tags.compliance }}
- name: AGENTS_COMPLIANCE
value: {{ . | quote }}
{{- end }}
{{- with .Values.config.oidc.providerArn }}
- name: AGENTS_OIDC_PROVIDER_ARN
value: {{ . | quote }}
Expand Down
10 changes: 10 additions & 0 deletions charts/operator/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,16 @@ config:
ssmPathPrefix: /eks-agent-platform
environment: ""
region: us-west-2
# Org-dimension tag values stamped on the tenant IRSA roles the operator
# creates (resource-tagging standard, required tier). Empty falls back to the
# operator's defaults (platform-engineering / engineering / internal / soc2);
# override per-env via values-<env>.yaml (e.g. dataClassification: confidential
# in production).
tags:
costCenter: ""
businessUnit: ""
dataClassification: ""
compliance: ""
# EKS OIDC wiring — required by PlatformReconciler.ensureIamRole to mint
# tenant IRSA trust policies. Set per-env values; the ApplicationSet
# passes them in via the values-<env>.yaml file resolved from the
Expand Down
16 changes: 16 additions & 0 deletions operators/cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,14 @@ func main() {
var oidcIssuerHost string
var disableAWS bool

// Org-dimension tag values stamped on tenant IRSA roles (resource-tagging
// standard). Env-level constants for the cluster the operator serves;
// tenantRoleTags falls back to landing-zone env.hcl defaults when unset.
var costCenter string
var businessUnit string
var dataClassification string
var compliance string

flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "Address the metric endpoint binds to.")
flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "Address the health probe binds to.")
flag.BoolVar(&enableLeaderElection, "leader-elect", true, "Enable leader election.")
Expand All @@ -99,6 +107,10 @@ func main() {
flag.StringVar(&region, "region", os.Getenv("AGENTS_REGION"), "AWS region. Defaults to credential-chain region if empty.")
flag.StringVar(&oidcProviderARN, "oidc-provider-arn", os.Getenv("AGENTS_OIDC_PROVIDER_ARN"), "EKS cluster OIDC provider ARN; used in tenant IRSA trust policies.")
flag.StringVar(&oidcIssuerHost, "oidc-issuer-host", os.Getenv("AGENTS_OIDC_ISSUER_HOST"), "EKS OIDC issuer host (oidc.eks.<region>.amazonaws.com/id/<id>).")
flag.StringVar(&costCenter, "cost-center", os.Getenv("AGENTS_COST_CENTER"), "Org cost-center tag stamped on tenant IRSA roles (resource-tagging standard).")
flag.StringVar(&businessUnit, "business-unit", os.Getenv("AGENTS_BUSINESS_UNIT"), "Org business-unit tag stamped on tenant IRSA roles.")
flag.StringVar(&dataClassification, "data-classification", os.Getenv("AGENTS_DATA_CLASSIFICATION"), "Org data-classification tag stamped on tenant IRSA roles.")
flag.StringVar(&compliance, "compliance", os.Getenv("AGENTS_COMPLIANCE"), "Org compliance tag stamped on tenant IRSA roles.")
flag.BoolVar(&disableAWS, "disable-aws", false, "Skip AWS client init + SSM config load (k8s-side reconciliation only).")
opts := zap.Options{Development: false}
opts.BindFlags(flag.CommandLine)
Expand Down Expand Up @@ -161,6 +173,10 @@ func main() {
OIDCProviderARN: oidcProviderARN,
OIDCIssuerHost: oidcIssuerHost,
Environment: environment,
CostCenter: costCenter,
BusinessUnit: businessUnit,
DataClassification: dataClassification,
Compliance: compliance,
}
platformReconciler.AWSCfg = controller.PlatformAWSConfig{
// cmk-data ARN isn't in operatorconfig today; the operator reads
Expand Down
58 changes: 51 additions & 7 deletions operators/internal/controller/platform_iam.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,56 @@ type IAMConfig struct {
OIDCProviderARN string
OIDCIssuerHost string // e.g. oidc.eks.us-west-2.amazonaws.com/id/EXAMPLE
Environment string

// Org-dimension tag values for tenant IRSA roles (resource-tagging
// standard, required tier). Sourced from the operator's deploy config
// (AGENTS_COST_CENTER / _BUSINESS_UNIT / _DATA_CLASSIFICATION / _COMPLIANCE).
// tenantRoleTags falls back to the landing-zone env.hcl defaults when these
// are unset, so a tenant role always carries the keys cloudgov gates on.
CostCenter string
BusinessUnit string
DataClassification string
Compliance string
}

// orDefault returns def when v is empty.
func orDefault(v, def string) string {
if v == "" {
return def
}
return v
}

// tenantRoleTags builds the IAM tag set for a tenant IRSA role.
//
// It preserves the keys the rest of the system depends on and must not rename:
// PlatformId (the BudgetPolicy reconciler groups Cost Explorer by it), Tenant,
// and Persona. On top of those it carries the required-tier resource-tagging
// keys cloudgov gates on — Project, Repository, Component, Team, CostCenter,
// BusinessUnit, DataClassification, Compliance — plus Environment and ManagedBy.
// ManagedBy is "eks-agent-platform" (the operator owns these roles' lifecycle,
// unlike the opentofu-managed roles in landing-zone).
func tenantRoleTags(p *platformv1alpha1.Platform, cfg IAMConfig) []iamtypes.Tag {
tag := func(k, v string) iamtypes.Tag {
return iamtypes.Tag{Key: aws.String(k), Value: aws.String(v)}
}
return []iamtypes.Tag{
// Load-bearing keys — PlatformId drives BudgetPolicy cost attribution.
tag("PlatformId", p.Name),
tag("Tenant", p.Spec.Tenant),
tag("Persona", p.Spec.Persona),
// Required-tier resource-tagging keys.
tag("Environment", cfg.Environment),
tag("ManagedBy", "eks-agent-platform"),
tag("Project", "eks-agent-platform"),
tag("Repository", "nanohype/eks-agent-platform"),
tag("Component", "tenant-iam"),
tag("Team", p.Spec.Tenant),
tag("CostCenter", orDefault(cfg.CostCenter, "platform-engineering")),
tag("BusinessUnit", orDefault(cfg.BusinessUnit, "engineering")),
tag("DataClassification", orDefault(cfg.DataClassification, "internal")),
tag("Compliance", orDefault(cfg.Compliance, "soc2")),
}
}

// ensureIamRole creates (or no-ops if already present) the tenant IRSA
Expand Down Expand Up @@ -191,13 +241,7 @@ func (r *PlatformReconciler) ensureIamRole(ctx context.Context, p *platformv1alp
Path: aws.String(path),
AssumeRolePolicyDocument: aws.String(trust),
Description: aws.String(fmt.Sprintf("Tenant IRSA role for Platform %s (tenant %s)", p.Name, p.Spec.Tenant)),
Tags: []iamtypes.Tag{
{Key: aws.String("PlatformId"), Value: aws.String(p.Name)},
{Key: aws.String("Tenant"), Value: aws.String(p.Spec.Tenant)},
{Key: aws.String("Persona"), Value: aws.String(p.Spec.Persona)},
{Key: aws.String("Environment"), Value: aws.String(cfg.Environment)},
{Key: aws.String("ManagedBy"), Value: aws.String("eks-agent-platform")},
},
Tags: tenantRoleTags(p, cfg),
}
if cfg.TenantPermissionsBoundaryARN != "" {
createInput.PermissionsBoundary = aws.String(cfg.TenantPermissionsBoundaryARN)
Expand Down
51 changes: 51 additions & 0 deletions operators/internal/controller/platform_iam_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import (

"github.com/aws/aws-sdk-go-v2/aws"
iamtypes "github.com/aws/aws-sdk-go-v2/service/iam/types"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

platformv1alpha1 "github.com/nanohype/eks-agent-platform/operators/api/platform/v1alpha1"
)

func TestSuspensionFromTags(t *testing.T) {
Expand Down Expand Up @@ -48,3 +51,51 @@ func TestSuspensionFromTags(t *testing.T) {
})
}
}

func tagMap(tags []iamtypes.Tag) map[string]string {
m := make(map[string]string, len(tags))
for _, t := range tags {
m[aws.ToString(t.Key)] = aws.ToString(t.Value)
}
return m
}

func TestTenantRoleTags(t *testing.T) {
p := &platformv1alpha1.Platform{
ObjectMeta: metav1.ObjectMeta{Name: "acme"},
Spec: platformv1alpha1.PlatformSpec{Tenant: "acme-team", Persona: "founder"},
}

// Empty org-dim config: the required keys must still be present (defaults).
got := tagMap(tenantRoleTags(p, IAMConfig{Environment: "production"}))

// The required-tier resource-tagging keys cloudgov gates on, plus the
// load-bearing PlatformId / Tenant / Persona the rest of the system reads.
for _, k := range []string{
"Environment", "ManagedBy", "Project", "Repository", "Component", "Team",
"CostCenter", "BusinessUnit", "DataClassification", "Compliance",
"PlatformId", "Tenant", "Persona",
} {
if got[k] == "" {
t.Errorf("tenantRoleTags missing/empty key %q (have %v)", k, got)
}
}
if got["PlatformId"] != "acme" {
t.Errorf("PlatformId: got %q want acme", got["PlatformId"])
}
if got["ManagedBy"] != "eks-agent-platform" {
t.Errorf("ManagedBy: got %q want eks-agent-platform", got["ManagedBy"])
}
if got["CostCenter"] != "platform-engineering" {
t.Errorf("CostCenter default: got %q want platform-engineering", got["CostCenter"])
}

// Explicit config wins over the defaults.
got = tagMap(tenantRoleTags(p, IAMConfig{
Environment: "dev", CostCenter: "research", BusinessUnit: "labs",
DataClassification: "confidential", Compliance: "hipaa",
}))
if got["CostCenter"] != "research" || got["Compliance"] != "hipaa" {
t.Errorf("config override not applied: %v", got)
}
}
Loading