diff --git a/.gitignore b/.gitignore index 7b10e193..380666f2 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,11 @@ test-results/ .specstory/ chart/chartsmith/*.tgz .direnv/ + +# Terraform +**/.terraform/ +**/.terraform.lock.hcl +**/terraform.tfvars +**/terraform.tfstate +**/terraform.tfstate.backup +**/.terraform.tfvars.backup diff --git a/.stignore b/.stignore deleted file mode 100644 index 326b3995..00000000 --- a/.stignore +++ /dev/null @@ -1,56 +0,0 @@ -.git -# Runtime data -pids -*.pid -*.seed -*.pid.lock - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# Bower dependency directory (https://bower.io/) -bower_components - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release - -# Dependency directories -node_modules -jspm_packages - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - -# parcel-bundler cache (https://parceljs.org/) -.cache - -# next.js build output -.next - -# nuxt.js build output -.nuxt - -# vuepress build output -.vuepress/dist - -# Serverless directories -.serverless - diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index f9479fff..00000000 --- a/CLAUDE.md +++ /dev/null @@ -1,12 +0,0 @@ -See the following files for details: - -ARCHITECTURE.md: Our core design principles -chartsmith-app/ARCHITECTURE.md: Our design principles for the frontend - -CONTRIBUTING.md: How to run and test this project -chartsmith-app/CONTRIBUTING.md: How to run and test the frontend - -- `/chart.go:106:2: declared and not used: repoUrl -pkg/workspace/chart.go:169:41: cannot use conn (variable of type *pgxpool.Conn) as *pgxpool.Pool value in argument to updatePublishStatus -pkg/workspace/chart.go:178:40: cannot use conn (variable of type *pgxpool.Conn) as *pgxpool.Pool value in argument to updatePublishStatus -make: *** [build] Error 1` \ No newline at end of file diff --git a/cmd/artifacthub.go b/cmd/artifacthub.go index 1ded8f56..ba4f481a 100644 --- a/cmd/artifacthub.go +++ b/cmd/artifacthub.go @@ -89,7 +89,7 @@ func runArtifactHubCache(ctx context.Context, pgURI string, force bool, verbose if err != nil && err != pgx.ErrNoRows { return fmt.Errorf("failed to get last updated time: %w", err) } - + if lastUpdated.Valid { // If cache was updated in the last 6 hours, skip if time.Since(lastUpdated.Time) < 6*time.Hour { @@ -136,7 +136,7 @@ func runArtifactHubCache(ctx context.Context, pgURI string, force bool, verbose if err != nil { return fmt.Errorf("failed to drop artifacthub_chart table: %w", err) } - + _, err = tx.Exec(ctx, ` CREATE TABLE artifacthub_chart ( id TEXT PRIMARY KEY, @@ -186,32 +186,32 @@ func runArtifactHubCache(ctx context.Context, pgURI string, force bool, verbose // Insert new data batchSize := 1000 inserted := 0 - + // Use a map to deduplicate packages with the same name+version deduplicated := make(map[string]HarborPackage) for _, pkg := range packages { key := fmt.Sprintf("%s-%s", pkg.Package, pkg.Version) deduplicated[key] = pkg } - + // Convert back to slice for batch processing uniquePackages := make([]HarborPackage, 0, len(deduplicated)) for _, pkg := range deduplicated { uniquePackages = append(uniquePackages, pkg) } - + // Group packages by name to get the latest version chartsByName := make(map[string][]HarborPackage) for _, pkg := range uniquePackages { chartsByName[pkg.Package] = append(chartsByName[pkg.Package], pkg) } - + if verbose { logger.Debug(fmt.Sprintf("Found %d unique chart names after deduplication", len(chartsByName))) - logger.Debug(fmt.Sprintf("Processing %d unique packages (removed %d duplicates)", - len(uniquePackages), len(packages) - len(uniquePackages))) + logger.Debug(fmt.Sprintf("Processing %d unique packages (removed %d duplicates)", + len(uniquePackages), len(packages)-len(uniquePackages))) } - + // Process in batches to avoid memory issues for _, batch := range createBatches(uniquePackages, batchSize) { _, err = tx.CopyFrom( @@ -231,11 +231,11 @@ func runArtifactHubCache(ctx context.Context, pgURI string, force bool, verbose }, nil }), ) - + if err != nil { return fmt.Errorf("failed to insert chart data batch: %w", err) } - + inserted += len(batch) if verbose { logger.Debug(fmt.Sprintf("Inserted %d/%d unique packages", inserted, len(uniquePackages))) @@ -264,7 +264,7 @@ func runArtifactHubCache(ctx context.Context, pgURI string, force bool, verbose func createBatches(items []HarborPackage, batchSize int) [][]HarborPackage { var batches [][]HarborPackage - + for i := 0; i < len(items); i += batchSize { end := i + batchSize if end > len(items) { @@ -272,6 +272,6 @@ func createBatches(items []HarborPackage, batchSize int) [][]HarborPackage { } batches = append(batches, items[i:end]) } - + return batches -} \ No newline at end of file +} diff --git a/cmd/debug-console.go b/cmd/debug-console.go index dd7a3e38..b743e752 100644 --- a/cmd/debug-console.go +++ b/cmd/debug-console.go @@ -15,7 +15,7 @@ import ( func DebugConsoleCmd() *cobra.Command { var workspaceID string var nonInteractive bool - + cmd := &cobra.Command{ Use: "debug-console [command] [flags]", Short: "Interactive debug console for chartsmith", @@ -71,7 +71,7 @@ Examples: return debugcli.RunConsole(opts) }, } - + // Add flags cmd.Flags().StringVar(&workspaceID, "workspace-id", "", "Workspace ID to use for commands") diff --git a/cmd/run.go b/cmd/run.go index d5d2b983..e20d7ea6 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -70,7 +70,7 @@ func runWorker(ctx context.Context, pgURI string) error { // Start the connection heartbeat before starting the listeners // This ensures our connections stay alive even during idle periods listener.StartHeartbeat(ctx) - + if err := listener.StartListeners(ctx); err != nil { return fmt.Errorf("failed to start listeners: %w", err) } diff --git a/design/convert-k8s-to-helm/README.md b/design/convert-k8s-to-helm/README.md deleted file mode 100644 index 14728d40..00000000 --- a/design/convert-k8s-to-helm/README.md +++ /dev/null @@ -1,12 +0,0 @@ -Here's how we will convert k8s manifests to helm charts. - - -1. Pre-processing: - - If a manifest is not a valid k8s manifest (kubeval), discard it - - Sort manifests by GVK, we want to order them: [configmaps, secrets, other] -2. Conversion Loop: - - Iterate through the manifests, sending each on to the LLM along with the aggregated values.yaml so far. Store the output of the values.yaml at each step. - - The LLM will return a helm template, and an updated values.yaml -3. Post-processing: - - Once completed, send the final values.yaml back to the LLM to be simplified. - - Send each manifest back to the LLM to be refactored to use the values.yaml now. diff --git a/design/post-plan-render/README.md b/design/post-plan-render/README.md deleted file mode 100644 index 2591379a..00000000 --- a/design/post-plan-render/README.md +++ /dev/null @@ -1,14 +0,0 @@ -When a plan is executed, ChartSmith should optimistically render the changes before the user decides to accept or reject them - -When we finish creating a revision, there are pending patches we show the user. -We should kick off a background render where we've created a "shadow" copy of the application with all patches accepted. -If there are errors on this render, we should show the error with an "attempt fix" button. - -When the user accepts the changes, there's nothing else for us to do. -If the user rejects some/any of the changes, we need to re-render. Once a single change is rejected, we should not re-render until there are no more pending changes. - -When rendering automatically with pending patches, we add an "is_autorender" set to true in the workspace_render table. -These are filtered out by default from the front end. -This allows the happy-path of iterate -> accept to work without cluttering the UI up with render. -But when it fails, the UI will show it with an Attempt Fix button. - diff --git a/go.work.sum b/go.work.sum index 3dbdf987..cef0d709 100644 --- a/go.work.sum +++ b/go.work.sum @@ -1,27 +1,140 @@ cel.dev/expr v0.18.0 h1:CJ6drgk+Hf96lkLikr4rFf19WrU0BOWEihyZnI2TAzo= cel.dev/expr v0.18.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= +cel.dev/expr v0.19.1/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= cloud.google.com/go v0.116.0 h1:B3fRrSDkLRt5qSHWe40ERJvhvnQwdZiHu0bJOpldweE= cloud.google.com/go v0.116.0/go.mod h1:cEPSRWPzZEswwdr9BxE6ChEn01dWlTaF05LiC2Xs70U= +cloud.google.com/go/accessapproval v1.8.2/go.mod h1:aEJvHZtpjqstffVwF/2mCXXSQmpskyzvw6zKLvLutZM= +cloud.google.com/go/accesscontextmanager v1.9.2/go.mod h1:T0Sw/PQPyzctnkw1pdmGAKb7XBA84BqQzH0fSU7wzJU= +cloud.google.com/go/aiplatform v1.69.0/go.mod h1:nUsIqzS3khlnWvpjfJbP+2+h+VrFyYsTm7RNCAViiY8= +cloud.google.com/go/analytics v0.25.2/go.mod h1:th0DIunqrhI1ZWVlT3PH2Uw/9ANX8YHfFDEPqf/+7xM= +cloud.google.com/go/apigateway v1.7.2/go.mod h1:+weId+9aR9J6GRwDka7jIUSrKEX60XGcikX7dGU8O7M= +cloud.google.com/go/apigeeconnect v1.7.2/go.mod h1:he/SWi3A63fbyxrxD6jb67ak17QTbWjva1TFbT5w8Kw= +cloud.google.com/go/apigeeregistry v0.9.2/go.mod h1:A5n/DwpG5NaP2fcLYGiFA9QfzpQhPRFNATO1gie8KM8= +cloud.google.com/go/appengine v1.9.2/go.mod h1:bK4dvmMG6b5Tem2JFZcjvHdxco9g6t1pwd3y/1qr+3s= +cloud.google.com/go/area120 v0.9.2/go.mod h1:Ar/KPx51UbrTWGVGgGzFnT7hFYQuk/0VOXkvHdTbQMI= +cloud.google.com/go/artifactregistry v1.16.0/go.mod h1:LunXo4u2rFtvJjrGjO0JS+Gs9Eco2xbZU6JVJ4+T8Sk= +cloud.google.com/go/asset v1.20.3/go.mod h1:797WxTDwdnFAJzbjZ5zc+P5iwqXc13yO9DHhmS6wl+o= +cloud.google.com/go/assuredworkloads v1.12.2/go.mod h1:/WeRr/q+6EQYgnoYrqCVgw7boMoDfjXZZev3iJxs2Iw= cloud.google.com/go/auth v0.13.0 h1:8Fu8TZy167JkW8Tj3q7dIkr2v4cndv41ouecJx0PAHs= cloud.google.com/go/auth v0.13.0/go.mod h1:COOjD9gwfKNKz+IIduatIhYJQIc0mG3H102r/EMxX6Q= cloud.google.com/go/auth/oauth2adapt v0.2.6 h1:V6a6XDu2lTwPZWOawrAa9HUK+DB2zfJyTuciBG5hFkU= cloud.google.com/go/auth/oauth2adapt v0.2.6/go.mod h1:AlmsELtlEBnaNTL7jCj8VQFLy6mbZv0s4Q7NGBeQ5E8= +cloud.google.com/go/automl v1.14.2/go.mod h1:mIat+Mf77W30eWQ/vrhjXsXaRh8Qfu4WiymR0hR6Uxk= +cloud.google.com/go/baremetalsolution v1.3.2/go.mod h1:3+wqVRstRREJV/puwaKAH3Pnn7ByreZG2aFRsavnoBQ= +cloud.google.com/go/batch v1.11.2/go.mod h1:ehsVs8Y86Q4K+qhEStxICqQnNqH8cqgpCxx89cmU5h4= +cloud.google.com/go/beyondcorp v1.1.2/go.mod h1:q6YWSkEsSZTU2WDt1qtz6P5yfv79wgktGtNbd0FJTLI= +cloud.google.com/go/bigquery v1.64.0/go.mod h1:gy8Ooz6HF7QmA+TRtX8tZmXBKH5mCFBwUApGAb3zI7Y= +cloud.google.com/go/bigtable v1.33.0/go.mod h1:HtpnH4g25VT1pejHRtInlFPnN5sjTxbQlsYBjh9t5l0= +cloud.google.com/go/billing v1.19.2/go.mod h1:AAtih/X2nka5mug6jTAq8jfh1nPye0OjkHbZEZgU59c= +cloud.google.com/go/binaryauthorization v1.9.2/go.mod h1:T4nOcRWi2WX4bjfSRXJkUnpliVIqjP38V88Z10OvEv4= +cloud.google.com/go/certificatemanager v1.9.2/go.mod h1:PqW+fNSav5Xz8bvUnJpATIRo1aaABP4mUg/7XIeAn6c= +cloud.google.com/go/channel v1.19.1/go.mod h1:ungpP46l6XUeuefbA/XWpWWnAY3897CSRPXUbDstwUo= +cloud.google.com/go/cloudbuild v1.19.0/go.mod h1:ZGRqbNMrVGhknIIjwASa6MqoRTOpXIVMSI+Ew5DMPuY= +cloud.google.com/go/clouddms v1.8.2/go.mod h1:pe+JSp12u4mYOkwXpSMouyCCuQHL3a6xvWH2FgOcAt4= +cloud.google.com/go/cloudtasks v1.13.2/go.mod h1:2pyE4Lhm7xY8GqbZKLnYk7eeuh8L0JwAvXx1ecKxYu8= cloud.google.com/go/compute v1.24.0 h1:phWcR2eWzRJaL/kOiJwfFsPs4BaKq1j6vnpZrc1YlVg= cloud.google.com/go/compute v1.24.0/go.mod h1:kw1/T+h/+tK2LJK0wiPPx1intgdAM3j/g3hFDlscY40= +cloud.google.com/go/compute v1.29.0/go.mod h1:HFlsDurE5DpQZClAGf/cYh+gxssMhBxBovZDYkEn/Og= cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I= cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= +cloud.google.com/go/contactcenterinsights v1.15.1/go.mod h1:cFGxDVm/OwEVAHbU9UO4xQCtQFn0RZSrSUcF/oJ0Bbs= +cloud.google.com/go/container v1.42.0/go.mod h1:YL6lDgCUi3frIWNIFU9qrmF7/6K1EYrtspmFTyyqJ+k= +cloud.google.com/go/containeranalysis v0.13.2/go.mod h1:AiKvXJkc3HiqkHzVIt6s5M81wk+q7SNffc6ZlkTDgiE= +cloud.google.com/go/datacatalog v1.23.0/go.mod h1:9Wamq8TDfL2680Sav7q3zEhBJSPBrDxJU8WtPJ25dBM= +cloud.google.com/go/dataflow v0.10.2/go.mod h1:+HIb4HJxDCZYuCqDGnBHZEglh5I0edi/mLgVbxDf0Ag= +cloud.google.com/go/dataform v0.10.2/go.mod h1:oZHwMBxG6jGZCVZqqMx+XWXK+dA/ooyYiyeRbUxI15M= +cloud.google.com/go/datafusion v1.8.2/go.mod h1:XernijudKtVG/VEvxtLv08COyVuiYPraSxm+8hd4zXA= +cloud.google.com/go/datalabeling v0.9.2/go.mod h1:8me7cCxwV/mZgYWtRAd3oRVGFD6UyT7hjMi+4GRyPpg= +cloud.google.com/go/dataplex v1.19.2/go.mod h1:vsxxdF5dgk3hX8Ens9m2/pMNhQZklUhSgqTghZtF1v4= +cloud.google.com/go/dataproc/v2 v2.10.0/go.mod h1:HD16lk4rv2zHFhbm8gGOtrRaFohMDr9f0lAUMLmg1PM= +cloud.google.com/go/dataqna v0.9.2/go.mod h1:WCJ7pwD0Mi+4pIzFQ+b2Zqy5DcExycNKHuB+VURPPgs= +cloud.google.com/go/datastore v1.20.0/go.mod h1:uFo3e+aEpRfHgtp5pp0+6M0o147KoPaYNaPAKpfh8Ew= +cloud.google.com/go/datastream v1.11.2/go.mod h1:RnFWa5zwR5SzHxeZGJOlQ4HKBQPcjGfD219Qy0qfh2k= +cloud.google.com/go/deploy v1.25.0/go.mod h1:h9uVCWxSDanXUereI5WR+vlZdbPJ6XGy+gcfC25v5rM= +cloud.google.com/go/dialogflow v1.60.0/go.mod h1:PjsrI+d2FI4BlGThxL0+Rua/g9vLI+2A1KL7s/Vo3pY= +cloud.google.com/go/dlp v1.20.0/go.mod h1:nrGsA3r8s7wh2Ct9FWu69UjBObiLldNyQda2RCHgdaY= +cloud.google.com/go/documentai v1.35.0/go.mod h1:ZotiWUlDE8qXSUqkJsGMQqVmfTMYATwJEYqbPXTR9kk= +cloud.google.com/go/domains v0.10.2/go.mod h1:oL0Wsda9KdJvvGNsykdalHxQv4Ri0yfdDkIi3bzTUwk= +cloud.google.com/go/edgecontainer v1.4.0/go.mod h1:Hxj5saJT8LMREmAI9tbNTaBpW5loYiWFyisCjDhzu88= +cloud.google.com/go/errorreporting v0.3.1/go.mod h1:6xVQXU1UuntfAf+bVkFk6nld41+CPyF2NSPCyXE3Ztk= +cloud.google.com/go/essentialcontacts v1.7.2/go.mod h1:NoCBlOIVteJFJU+HG9dIG/Cc9kt1K9ys9mbOaGPUmPc= +cloud.google.com/go/eventarc v1.15.0/go.mod h1:PAd/pPIZdJtJQFJI1yDEUms1mqohdNuM1BFEVHHlVFg= +cloud.google.com/go/filestore v1.9.2/go.mod h1:I9pM7Hoetq9a7djC1xtmtOeHSUYocna09ZP6x+PG1Xw= cloud.google.com/go/firestore v1.15.0 h1:/k8ppuWOtNuDHt2tsRV42yI21uaGnKDEQnRFeBpbFF8= cloud.google.com/go/firestore v1.15.0/go.mod h1:GWOxFXcv8GZUtYpWHw/w6IuYNux/BtmeVTMmjrm4yhk= +cloud.google.com/go/firestore v1.17.0/go.mod h1:69uPx1papBsY8ZETooc71fOhoKkD70Q1DwMrtKuOT/Y= +cloud.google.com/go/functions v1.19.2/go.mod h1:SBzWwWuaFDLnUyStDAMEysVN1oA5ECLbP3/PfJ9Uk7Y= +cloud.google.com/go/gkebackup v1.6.2/go.mod h1:WsTSWqKJkGan1pkp5dS30oxb+Eaa6cLvxEUxKTUALwk= +cloud.google.com/go/gkeconnect v0.12.0/go.mod h1:zn37LsFiNZxPN4iO7YbUk8l/E14pAJ7KxpoXoxt7Ly0= +cloud.google.com/go/gkehub v0.15.2/go.mod h1:8YziTOpwbM8LM3r9cHaOMy2rNgJHXZCrrmGgcau9zbQ= +cloud.google.com/go/gkemulticloud v1.4.1/go.mod h1:KRvPYcx53bztNwNInrezdfNF+wwUom8Y3FuJBwhvFpQ= +cloud.google.com/go/gsuiteaddons v1.7.2/go.mod h1:GD32J2rN/4APilqZw4JKmwV84+jowYYMkEVwQEYuAWc= cloud.google.com/go/iam v1.2.2 h1:ozUSofHUGf/F4tCNy/mu9tHLTaxZFLOUiKzjcgWHGIA= cloud.google.com/go/iam v1.2.2/go.mod h1:0Ys8ccaZHdI1dEUilwzqng/6ps2YB6vRsjIe00/+6JY= +cloud.google.com/go/iap v1.10.2/go.mod h1:cClgtI09VIfazEK6VMJr6bX8KQfuQ/D3xqX+d0wrUlI= +cloud.google.com/go/ids v1.5.2/go.mod h1:P+ccDD96joXlomfonEdCnyrHvE68uLonc7sJBPVM5T0= +cloud.google.com/go/iot v1.8.2/go.mod h1:UDwVXvRD44JIcMZr8pzpF3o4iPsmOO6fmbaIYCAg1ww= +cloud.google.com/go/kms v1.20.1/go.mod h1:LywpNiVCvzYNJWS9JUcGJSVTNSwPwi0vBAotzDqn2nc= +cloud.google.com/go/language v1.14.2/go.mod h1:dviAbkxT9art+2ioL9AM05t+3Ql6UPfMpwq1cDsF+rg= +cloud.google.com/go/lifesciences v0.10.2/go.mod h1:vXDa34nz0T/ibUNoeHnhqI+Pn0OazUTdxemd0OLkyoY= +cloud.google.com/go/logging v1.12.0/go.mod h1:wwYBt5HlYP1InnrtYI0wtwttpVU1rifnMT7RejksUAM= cloud.google.com/go/longrunning v0.5.5 h1:GOE6pZFdSrTb4KAiKnXsJBtlE6mEyaW44oKyMILWnOg= cloud.google.com/go/longrunning v0.5.5/go.mod h1:WV2LAxD8/rg5Z1cNW6FJ/ZpX4E4VnDnoTk0yawPBB7s= +cloud.google.com/go/longrunning v0.6.2/go.mod h1:k/vIs83RN4bE3YCswdXC5PFfWVILjm3hpEUlSko4PiI= +cloud.google.com/go/managedidentities v1.7.2/go.mod h1:t0WKYzagOoD3FNtJWSWcU8zpWZz2i9cw2sKa9RiPx5I= +cloud.google.com/go/maps v1.15.0/go.mod h1:ZFqZS04ucwFiHSNU8TBYDUr3wYhj5iBFJk24Ibvpf3o= +cloud.google.com/go/mediatranslation v0.9.2/go.mod h1:1xyRoDYN32THzy+QaU62vIMciX0CFexplju9t30XwUc= +cloud.google.com/go/memcache v1.11.2/go.mod h1:jIzHn79b0m5wbkax2SdlW5vNSbpaEk0yWHbeLpMIYZE= +cloud.google.com/go/metastore v1.14.2/go.mod h1:dk4zOBhZIy3TFOQlI8sbOa+ef0FjAcCHEnd8dO2J+LE= cloud.google.com/go/monitoring v1.21.2 h1:FChwVtClH19E7pJ+e0xUhJPGksctZNVOk2UhMmblmdU= cloud.google.com/go/monitoring v1.21.2/go.mod h1:hS3pXvaG8KgWTSz+dAdyzPrGUYmi2Q+WFX8g2hqVEZU= +cloud.google.com/go/networkconnectivity v1.15.2/go.mod h1:N1O01bEk5z9bkkWwXLKcN2T53QN49m/pSpjfUvlHDQY= +cloud.google.com/go/networkmanagement v1.16.0/go.mod h1:Yc905R9U5jik5YMt76QWdG5WqzPU4ZsdI/mLnVa62/Q= +cloud.google.com/go/networksecurity v0.10.2/go.mod h1:puU3Gwchd6Y/VTyMkL50GI2RSRMS3KXhcDBY1HSOcck= +cloud.google.com/go/notebooks v1.12.2/go.mod h1:EkLwv8zwr8DUXnvzl944+sRBG+b73HEKzV632YYAGNI= +cloud.google.com/go/optimization v1.7.2/go.mod h1:msYgDIh1SGSfq6/KiWJQ/uxMkWq8LekPyn1LAZ7ifNE= +cloud.google.com/go/orchestration v1.11.1/go.mod h1:RFHf4g88Lbx6oKhwFstYiId2avwb6oswGeAQ7Tjjtfw= +cloud.google.com/go/orgpolicy v1.14.1/go.mod h1:1z08Hsu1mkoH839X7C8JmnrqOkp2IZRSxiDw7W/Xpg4= +cloud.google.com/go/osconfig v1.14.2/go.mod h1:kHtsm0/j8ubyuzGciBsRxFlbWVjc4c7KdrwJw0+g+pQ= +cloud.google.com/go/oslogin v1.14.2/go.mod h1:M7tAefCr6e9LFTrdWRQRrmMeKHbkvc4D9g6tHIjHySA= +cloud.google.com/go/phishingprotection v0.9.2/go.mod h1:mSCiq3tD8fTJAuXq5QBHFKZqMUy8SfWsbUM9NpzJIRQ= +cloud.google.com/go/policytroubleshooter v1.11.2/go.mod h1:1TdeCRv8Qsjcz2qC3wFltg/Mjga4HSpv8Tyr5rzvPsw= +cloud.google.com/go/privatecatalog v0.10.2/go.mod h1:o124dHoxdbO50ImR3T4+x3GRwBSTf4XTn6AatP8MgsQ= +cloud.google.com/go/pubsub v1.45.1/go.mod h1:3bn7fTmzZFwaUjllitv1WlsNMkqBgGUb3UdMhI54eCc= +cloud.google.com/go/pubsublite v1.8.2/go.mod h1:4r8GSa9NznExjuLPEJlF1VjOPOpgf3IT6k8x/YgaOPI= +cloud.google.com/go/recaptchaenterprise/v2 v2.19.0/go.mod h1:vnbA2SpVPPwKeoFrCQxR+5a0JFRRytwBBG69Zj9pGfk= +cloud.google.com/go/recommendationengine v0.9.2/go.mod h1:DjGfWZJ68ZF5ZuNgoTVXgajFAG0yLt4CJOpC0aMK3yw= +cloud.google.com/go/recommender v1.13.2/go.mod h1:XJau4M5Re8F4BM+fzF3fqSjxNJuM66fwF68VCy/ngGE= +cloud.google.com/go/redis v1.17.2/go.mod h1:h071xkcTMnJgQnU/zRMOVKNj5J6AttG16RDo+VndoNo= +cloud.google.com/go/resourcemanager v1.10.2/go.mod h1:5f+4zTM/ZOTDm6MmPOp6BQAhR0fi8qFPnvVGSoWszcc= +cloud.google.com/go/resourcesettings v1.8.2/go.mod h1:uEgtPiMA+xuBUM4Exu+ZkNpMYP0BLlYeJbyNHfrc+U0= +cloud.google.com/go/retail v1.19.1/go.mod h1:W48zg0zmt2JMqmJKCuzx0/0XDLtovwzGAeJjmv6VPaE= +cloud.google.com/go/run v1.7.0/go.mod h1:IvJOg2TBb/5a0Qkc6crn5yTy5nkjcgSWQLhgO8QL8PQ= +cloud.google.com/go/scheduler v1.11.2/go.mod h1:GZSv76T+KTssX2I9WukIYQuQRf7jk1WI+LOcIEHUUHk= +cloud.google.com/go/secretmanager v1.14.2/go.mod h1:Q18wAPMM6RXLC/zVpWTlqq2IBSbbm7pKBlM3lCKsmjw= +cloud.google.com/go/security v1.18.2/go.mod h1:3EwTcYw8554iEtgK8VxAjZaq2unFehcsgFIF9nOvQmU= +cloud.google.com/go/securitycenter v1.35.2/go.mod h1:AVM2V9CJvaWGZRHf3eG+LeSTSissbufD27AVBI91C8s= +cloud.google.com/go/servicedirectory v1.12.2/go.mod h1:F0TJdFjqqotiZRlMXgIOzszaplk4ZAmUV8ovHo08M2U= +cloud.google.com/go/shell v1.8.2/go.mod h1:QQR12T6j/eKvqAQLv6R3ozeoqwJ0euaFSz2qLqG93Bs= +cloud.google.com/go/spanner v1.73.0/go.mod h1:mw98ua5ggQXVWwp83yjwggqEmW9t8rjs9Po1ohcUGW4= +cloud.google.com/go/speech v1.25.2/go.mod h1:KPFirZlLL8SqPaTtG6l+HHIFHPipjbemv4iFg7rTlYs= cloud.google.com/go/storage v1.49.0 h1:zenOPBOWHCnojRd9aJZAyQXBYqkJkdQS42dxL55CIMw= cloud.google.com/go/storage v1.49.0/go.mod h1:k1eHhhpLvrPjVGfo0mOUPEJ4Y2+a/Hv5PiwehZI9qGU= -github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20230306123547-8075edf89bb0 h1:59MxjQVfjXsBpLy+dbd2/ELV5ofnUkUZBvWSC85sheA= -github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20230306123547-8075edf89bb0/go.mod h1:OahwfttHWG6eJ0clwcfBAHoDI6X/LV/15hx/wlMZSrU= +cloud.google.com/go/storagetransfer v1.11.2/go.mod h1:FcM29aY4EyZ3yVPmW5SxhqUdhjgPBUOFyy4rqiQbias= +cloud.google.com/go/talent v1.7.2/go.mod h1:k1sqlDgS9gbc0gMTRuRQpX6C6VB7bGUxSPcoTRWJod8= +cloud.google.com/go/texttospeech v1.10.0/go.mod h1:215FpCOyRxxrS7DSb2t7f4ylMz8dXsQg8+Vdup5IhP4= +cloud.google.com/go/tpu v1.7.2/go.mod h1:0Y7dUo2LIbDUx0yQ/vnLC6e18FK6NrDfAhYS9wZ/2vs= +cloud.google.com/go/trace v1.11.2/go.mod h1:bn7OwXd4pd5rFuAnTrzBuoZ4ax2XQeG3qNgYmfCy0Io= +cloud.google.com/go/translate v1.12.2/go.mod h1:jjLVf2SVH2uD+BNM40DYvRRKSsuyKxVvs3YjTW/XSWY= +cloud.google.com/go/video v1.23.2/go.mod h1:rNOr2pPHWeCbW0QsOwJRIe0ZiuwHpHtumK0xbiYB1Ew= +cloud.google.com/go/videointelligence v1.12.2/go.mod h1:8xKGlq0lNVyT8JgTkkCUCpyNJnYYEJVWGdqzv+UcwR8= +cloud.google.com/go/vision/v2 v2.9.2/go.mod h1:WuxjVQdAy4j4WZqY5Rr655EdAgi8B707Vdb5T8c90uo= +cloud.google.com/go/vmmigration v1.8.2/go.mod h1:FBejrsr8ZHmJb949BSOyr3D+/yCp9z9Hk0WtsTiHc1Q= +cloud.google.com/go/vmwareengine v1.3.2/go.mod h1:JsheEadzT0nfXOGkdnwtS1FhFAnj4g8qhi4rKeLi/AU= +cloud.google.com/go/vpcaccess v1.8.2/go.mod h1:4yvYKNjlNjvk/ffgZ0PuEhpzNJb8HybSM1otG2aDxnY= +cloud.google.com/go/webrisk v1.10.2/go.mod h1:c0ODT2+CuKCYjaeHO7b0ni4CUrJ95ScP5UFl9061Qq8= +cloud.google.com/go/websecurityscanner v1.7.2/go.mod h1:728wF9yz2VCErfBaACA5px2XSYHQgkK812NmHcUsDXA= +cloud.google.com/go/workflows v1.13.2/go.mod h1:l5Wj2Eibqba4BsADIRzPLaevLmIuYF2W+wfFBkRG3vU= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0 h1:3c8yed4lgqTt+oTQ+JNMDo+F4xprBf+O/il4ZC0nRLw= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0/go.mod h1:obipzmGjfSjam60XLwGfqUkJsfiheAl+TUjG+4yzyPM= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.48.1 h1:UQ0AhxogsIRZDkElkblfnwjc3IaltCm2HUMvezQaL7s= @@ -32,8 +145,10 @@ github.com/Masterminds/vcs v1.13.3 h1:IIA2aBdXvfbIM+yl/eTnL4hb1XwdpvuQLglAix1gwe github.com/Masterminds/vcs v1.13.3/go.mod h1:TiE7xuEjl1N4j016moRd6vezp6e6Lz23gypeXfzXeW8= github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I= github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= +github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q= github.com/agnivade/levenshtein v1.1.1 h1:QY8M92nrzkmr798gCo3kmMyqXFzdQVpxLlGPRBij0P8= github.com/agnivade/levenshtein v1.1.1/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo= +github.com/akavel/rsrc v0.10.2/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= github.com/alecthomas/kingpin/v2 v2.4.0 h1:f48lwail6p8zpO1bC4TxtqACaGqHYA22qkHjHpqDjYY= github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc h1:cAKDfWh5VpdgMhJosfJnn5/FoN2SRZ4p7fJNX58YPaU= @@ -87,8 +202,6 @@ github.com/chewxy/hm v1.0.0 h1:zy/TSv3LV2nD3dwUEQL2VhXeoXbb9QkpmdRAVUFiA6k= github.com/chewxy/hm v1.0.0/go.mod h1:qg9YI4q6Fkj/whwHR1D+bOGeF7SniIP40VweVepLjg0= github.com/chewxy/math32 v1.11.0 h1:8sek2JWqeaKkVnHa7bPVqCEOUPbARo4SGxs6toKyAOo= github.com/chewxy/math32 v1.11.0/go.mod h1:dOB2rcuFrCn6UHrze36WSLVPKtzPMRAQvBvUwkSsLqs= -github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= -github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= github.com/cilium/ebpf v0.9.1 h1:64sn2K3UKw8NbP/blsixRpF3nXuyhz/VjRlRzvlBRu4= github.com/cilium/ebpf v0.9.1/go.mod h1:+OhNOIXx/Fnu1IE8bJz2dzOA+VSfyTfdNUVdlQnxUFY= github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= @@ -107,8 +220,6 @@ github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARu github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= github.com/containerd/containerd/api v1.7.19 h1:VWbJL+8Ap4Ju2mx9c9qS1uFSB1OVYr5JJrW2yT5vFoA= github.com/containerd/containerd/api v1.7.19/go.mod h1:fwGavl3LNwAV5ilJ0sbrABL44AQxmNjDRcwheXDb6Ig= -github.com/containerd/fifo v1.1.0 h1:4I2mbh5stb1u6ycIABlBw9zgtlK8viPI9QkQNRQEEmY= -github.com/containerd/fifo v1.1.0/go.mod h1:bmC4NWMbXlt2EZ0Hc7Fx7QzTFxgPID13eH0Qu+MAb2o= github.com/containerd/go-cni v1.1.9 h1:ORi7P1dYzCwVM6XPN4n3CbkuOx/NZ2DOqy+SHRdo9rU= github.com/containerd/go-cni v1.1.9/go.mod h1:XYrZJ1d5W6E2VOvjffL3IZq0Dz6bsVlERHbekNK90PM= github.com/containerd/go-runc v1.0.0 h1:oU+lLv1ULm5taqgV/CJivypVODI4SUz1znWjv3nNYS0= @@ -117,12 +228,11 @@ github.com/containerd/imgcrypt v1.1.8 h1:ZS7TuywcRNLoHpU0g+v4/PsKynl6TYlw5xDVWWo github.com/containerd/imgcrypt v1.1.8/go.mod h1:x6QvFIkMyO2qGIY2zXc88ivEzcbgvLdWjoZyGqDap5U= github.com/containerd/nri v0.6.1 h1:xSQ6elnQ4Ynidm9u49ARK9wRKHs80HCUI+bkXOxV4mA= github.com/containerd/nri v0.6.1/go.mod h1:7+sX3wNx+LR7RzhjnJiUkFDhn18P5Bg/0VnJ/uXpRJM= +github.com/containerd/nri v0.8.0/go.mod h1:uSkgBrCdEtAiEz4vnrq8gmAC4EnVAM5Klt0OuK5rZYQ= +github.com/containerd/stargz-snapshotter/estargz v0.14.3/go.mod h1:KY//uOCIkSuNAHhJogcZtrNHdKrA99/FCCRjE3HD36o= github.com/containerd/ttrpc v1.2.5 h1:IFckT1EFQoFBMG4c3sMdT8EP3/aKfumK1msY+Ze4oLU= github.com/containerd/ttrpc v1.2.5/go.mod h1:YCXHsb32f+Sq5/72xHubdiJRQY9inL4a4ZQrAbN1q9o= -github.com/containerd/typeurl v1.0.2 h1:Chlt8zIieDbzQFzXzAeBEF92KhExuE4p9p92/QmY7aY= github.com/containerd/typeurl v1.0.2/go.mod h1:9trJWW2sRlGub4wZJRTW83VtbOLS6hwcDZXTn6oPz9s= -github.com/containerd/typeurl/v2 v2.1.1 h1:3Q4Pt7i8nYwy2KmQWIw2+1hTvwTE/6w9FqcttATPO/4= -github.com/containerd/typeurl/v2 v2.1.1/go.mod h1:IDp2JFvbwZ31H8dQbEIY7sDl2L3o3HZj1hsSQlywkQ0= github.com/containerd/zfs v1.1.0 h1:n7OZ7jZumLIqNJqXrEc/paBM840mORnmGdJDmAmJZHM= github.com/containerd/zfs v1.1.0/go.mod h1:oZF9wBnrnQjpWLaPKEinrx3TQ9a+W/RJO7Zb41d8YLE= github.com/containernetworking/cni v1.1.2 h1:wtRGZVv7olUHMOqouPpn3cXJWpJgM6+EUl31EQbXALQ= @@ -133,15 +243,16 @@ github.com/containers/ocicrypt v1.1.10 h1:r7UR6o8+lyhkEywetubUUgcKFjOWOaWz8cEBrC github.com/containers/ocicrypt v1.1.10/go.mod h1:YfzSSr06PTHQwSTUKqDSjish9BeW1E4HUmreluQcMd8= github.com/coreos/go-oidc v2.2.1+incompatible h1:mh48q/BqXqgjVHpy2ZY7WnWAbenxRjsz9N1i1YxjHAk= github.com/coreos/go-oidc v2.2.1+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= +github.com/coreos/go-oidc v2.3.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= -github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= -github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= github.com/d4l3k/go-bfloat16 v0.0.0-20211005043715-690c3bdd05f1 h1:cBzrdJPAFBsgCrDPnZxlp1dF2+k4r1kVpD7+1S1PVjY= github.com/d4l3k/go-bfloat16 v0.0.0-20211005043715-690c3bdd05f1/go.mod h1:uw2gLcxEuYUlAd/EXyjc/v55nd3+47YAgWbSXVxPrNI= github.com/danieljoos/wincred v1.1.2 h1:QLdCxFs1/Yl4zduvBdcHB8goaYk9RARS2SgLLRuAyr0= github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0= +github.com/danieljoos/wincred v1.2.1/go.mod h1:uGaFL9fDn3OLTvzCGulzE+SzjEe5NGlh5FdCcyfPwps= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210816181553-5444fa50b93d/go.mod h1:tmAIfUFEirG/Y8jhZ9M+h36obRZAk/1fcSpXwAVlfqE= github.com/denisenkom/go-mssqldb v0.9.0 h1:RSohk2RsiZqLZ0zCjtfn3S4Gp4exhpBWHyQ7D0yGjAk= github.com/denisenkom/go-mssqldb v0.9.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= @@ -156,6 +267,7 @@ github.com/fatih/camelcase v1.0.0 h1:hxNvNX/xYBp0ovncs8WyWZrOrpBNub/JfaMvbURyft8 github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc= github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gin-contrib/cors v1.7.2 h1:oLDHxdg8W/XDoN/8zamqk/Drgt4oVZDvaV0YmvVICQw= github.com/gin-contrib/cors v1.7.2/go.mod h1:SUJVARKgQ40dmrzgXEVxj2m7Ig1v1qIboQkPDTQ9t2E= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= @@ -164,6 +276,7 @@ github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7G7k= github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= +github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= github.com/go-kit/kit v0.8.0 h1:Wz+5lgoB0kkuqLEc6NVmwRknTKP6dTGbSqvhZtBI/j0= github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU= github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= @@ -190,14 +303,18 @@ github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang/glog v1.2.2 h1:1+mZ9upx1Dh6FmUTFR1naJ77miKiXgALjWOZ3NVFPmY= github.com/golang/glog v1.2.2/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/google/cel-go v0.22.0 h1:b3FJZxpiv1vTMo2/5RDUqAHPxkT8mmMfJIrq1llbf7g= github.com/google/cel-go v0.22.0/go.mod h1:BuznPXXfQDpXKWQ9sPW3TzlAJN5zzFe+i9tIs0yC4s8= +github.com/google/cel-go v0.23.2/go.mod h1:52Pb6QsDbC5kvgxvZhiL9QX1oZEkcUF/ZqaPx1J5Wwo= github.com/google/flatbuffers v24.3.25+incompatible h1:CX395cjN9Kke9mmalRoL3d81AtFUxJM+yDthflgJGkI= github.com/google/flatbuffers v24.3.25+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/go-containerregistry v0.14.0/go.mod h1:aiJ2fp/SXvkWgmYHioXnbMdlgB8eXiiYOY55gfN91Wk= github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM= github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA= github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw= @@ -225,8 +342,10 @@ github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= github.com/intel/goresctrl v0.3.0 h1:K2D3GOzihV7xSBedGxONSlaw/un1LZgWsc9IfqipN4c= github.com/intel/goresctrl v0.3.0/go.mod h1:fdz3mD85cmP9sHD8JUlrNWAxvwM86CrbmVXltEKd7zk= +github.com/intel/goresctrl v0.5.0/go.mod h1:mIe63ggylWYr0cU/l8n11FAkesqfvuP3oktIsxvu0T0= github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4= github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc= +github.com/josephspurrier/goversioninfo v1.4.0/go.mod h1:JWzv5rKQr+MmW+LvM412ToT/IkYDZjaclF2pKDss8IY= github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= @@ -240,8 +359,16 @@ github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY= github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y= +github.com/lestrrat-go/blackmagic v1.0.0/go.mod h1:TNgH//0vYSs8VXDCfkZLgIrVTTXQELZffUV0tz3MtdQ= +github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= +github.com/lestrrat-go/iter v1.0.1/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc= +github.com/lestrrat-go/jwx v1.2.25/go.mod h1:zoNuZymNl5lgdcu6P7K6ie2QRll5HVfF4xwxBBK1NxY= +github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= +github.com/linuxkit/virtsock v0.0.0-20201010232012-f8cee7dfc7a3/go.mod h1:3r6x7q95whyfWQpmGZTu3gk3v2YkMi05HEzl7Tf7YEo= github.com/lithammer/dedent v1.1.0 h1:VNzHMVCBNG1j0fh3OrsFRkVUwStdDArbgBWoPAffktY= github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc= github.com/mattn/go-oci8 v0.1.1 h1:aEUDxNAyDG0tv8CA3TArnDQNyc4EhnWlsfxRgDHABHM= @@ -260,8 +387,6 @@ github.com/mitchellh/cli v1.1.5 h1:OxRIeJXpAMztws/XHlN2vu6imG5Dpq+j61AzAX5fLng= github.com/mitchellh/cli v1.1.5/go.mod h1:v8+iFts2sPIKUV1ltktPXMCC8fumSKFItNcD2cLtRR4= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/moby/sys/signal v0.7.0 h1:25RW3d5TnQEoKvRbEKUGay6DCQ46IxAVTT9CUMgmsSI= -github.com/moby/sys/signal v0.7.0/go.mod h1:GQ6ObYZfqacOwTtlXvcmh9A26dVRul/hbOZn88Kg8Tg= github.com/moby/sys/symlink v0.2.0 h1:tk1rOM+Ljp0nFmfOIBtlV3rTDlWOwFRhjEeAhZB0nZc= github.com/moby/sys/symlink v0.2.0/go.mod h1:7uZVF2dqJjG/NsClqul95CqKOBRQyYSNnJ6BMgR/gFs= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= @@ -280,12 +405,10 @@ github.com/nlpodyssey/gopickle v0.3.0 h1:BLUE5gxFLyyNOPzlXxt6GoHEMMxD0qhsE4p0CIQ github.com/nlpodyssey/gopickle v0.3.0/go.mod h1:f070HJ/yR+eLi5WmM1OXJEGaTpuJEUiib19olXgYha0= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= -github.com/opencontainers/runtime-spec v1.1.0 h1:HHUyrt9mwHUjtasSbXSMvs4cyFxh+Bll4AjJ9odEGpg= -github.com/opencontainers/runtime-spec v1.1.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/open-policy-agent/opa v0.42.2/go.mod h1:MrmoTi/BsKWT58kXlVayBb+rYVeaMwuBm3nYAN3923s= +github.com/opencontainers/runc v1.1.5/go.mod h1:1J5XiS+vdZ3wCyZybsuxXZWGrgSr8fFJHLXuG2PsnNg= github.com/opencontainers/runtime-tools v0.9.1-0.20221107090550-2e043c6bd626 h1:DmNGcqH3WDbV5k8OJ+esPWbqUOX5rMLR2PMvziDMJi0= github.com/opencontainers/runtime-tools v0.9.1-0.20221107090550-2e043c6bd626/go.mod h1:BRHJJd0E+cx42OybVYSgUvZmU0B8P9gZuRXlZUP7TKI= -github.com/opencontainers/selinux v1.11.0 h1:+5Zbo97w3Lbmb3PeqQtpmTkMwsW5nRI3YaLpt7tQ7oU= -github.com/opencontainers/selinux v1.11.0/go.mod h1:E5dMC3VPuVvVHDYmi78qvhJp8+M586T4DlDRYpFkyec= github.com/pdevine/tensor v0.0.0-20240510204454-f88f4562727c h1:GwiUUjKefgvSNmv3NCvI/BL0kDebW6Xa+kcdpdc1mTY= github.com/pdevine/tensor v0.0.0-20240510204454-f88f4562727c/go.mod h1:PSojXDXF7TbgQiD6kkd98IHOS0QqTyUEaWRiS8+BLu8= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= @@ -298,10 +421,12 @@ github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXq github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= github.com/pquerna/cachecontrol v0.1.0 h1:yJMy84ti9h/+OEWa752kBTKv4XC30OtVVHYv/8cTqKc= github.com/pquerna/cachecontrol v0.1.0/go.mod h1:NrUG3Z7Rdu85UNR3vm7SOsl1nFIeSiQnrHV5K9mBcUI= +github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3VRLtww= github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY= github.com/sagikazarmark/crypt v0.19.0 h1:WMyLTjHBo64UvNcWqpzY3pbZTYgnemZU8FBZigKc42E= github.com/sagikazarmark/crypt v0.19.0/go.mod h1:c6vimRziqqERhtSe0MhIvzE1w54FrCHtrXb5NH/ja78= +github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY= github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e h1:MZM7FHLqUHYI0Y/mQAt3d2aYa0SiNms/hFqC9qJYolM= github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041 h1:llrF3Fs4018ePo4+G/HV/uQUqEI1HMDjCeOf2V6puPc= github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js= @@ -322,6 +447,9 @@ github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65E github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/urfave/cli v1.22.12 h1:igJgVw1JdKH+trcLWLeLwZjU9fEfPesQ+9/e4MQ44S8= github.com/urfave/cli v1.22.12/go.mod h1:sSBEIC79qR6OvcmsD4U3KABeOTxDqQtdDnaFuUN30b8= +github.com/vbatts/tar-split v0.11.2/go.mod h1:vV3ZuO2yWSVsz+pfFzDG/upWH1JhjOiEaWq6kXyQ3VI= +github.com/vektah/gqlparser/v2 v2.4.5/go.mod h1:flJWIR04IMQPGz+BXLrORkrARBxv/rtyIAFvd/MceW0= +github.com/veraison/go-cose v1.0.0-rc.1/go.mod h1:7ziE85vSq4ScFTg6wyoMXjucIGOf4JkFEZi/an96Ct4= github.com/vishvananda/netlink v1.2.1-beta.2 h1:Llsql0lnQEbHj0I1OuKyp8otXp0r3q0mPkuhwHfStVs= github.com/vishvananda/netlink v1.2.1-beta.2/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho= github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f h1:p4VB7kIXpOQvVn1ZaTIVp+3vuYAXFe3OJEvjbUYJLaA= @@ -332,29 +460,38 @@ github.com/xiang90/probing v0.0.0-20221125231312-a49e3df8f510 h1:S2dVYn90KE98chq github.com/xiang90/probing v0.0.0-20221125231312-a49e3df8f510/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xtgo/set v1.0.0 h1:6BCNBRv3ORNDQ7fyoJXRv+tstJz3m1JVFQErfeZz2pY= github.com/xtgo/set v1.0.0/go.mod h1:d3NHzGzSa0NmB2NhFyECA+QdRp29oEn2xbT+TpeFoM8= +github.com/yashtewari/glob-intersection v0.1.0/go.mod h1:LK7pIC3piUjovexikBbJ26Yml7g8xa5bsjfx2v1fwok= github.com/yuin/goldmark v1.2.1 h1:ruQGxdhGHe7FWOJPT0mKs5+pD2Xs1Bm/kdGlHO04FmM= go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0= go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I= go.etcd.io/etcd/api/v3 v3.5.16 h1:WvmyJVbjWqK4R1E+B12RRHz3bRGy9XVfh++MgbN+6n0= go.etcd.io/etcd/api/v3 v3.5.16/go.mod h1:1P4SlIP/VwkDmGo3OlOD7faPeP8KDIFhqvciH5EfN28= +go.etcd.io/etcd/api/v3 v3.5.21/go.mod h1:c3aH5wcvXv/9dqIw2Y810LDXJfhSYdHQ0vxmP3CCHVY= go.etcd.io/etcd/client/pkg/v3 v3.5.16 h1:ZgY48uH6UvB+/7R9Yf4x574uCO3jIx0TRDyetSfId3Q= go.etcd.io/etcd/client/pkg/v3 v3.5.16/go.mod h1:V8acl8pcEK0Y2g19YlOV9m9ssUe6MgiDSobSoaBAM0E= +go.etcd.io/etcd/client/pkg/v3 v3.5.21/go.mod h1:BgqT/IXPjK9NkeSDjbzwsHySX3yIle2+ndz28nVsjUs= go.etcd.io/etcd/client/v2 v2.305.16 h1:kQrn9o5czVNaukf2A2At43cE9ZtWauOtf9vRZuiKXow= go.etcd.io/etcd/client/v2 v2.305.16/go.mod h1:h9YxWCzcdvZENbfzBTFCnoNumr2ax3F19sKMqHFmXHE= +go.etcd.io/etcd/client/v2 v2.305.21/go.mod h1:OKkn4hlYNf43hpjEM3Ke3aRdUkhSl8xjKjSf8eCq2J8= go.etcd.io/etcd/client/v3 v3.5.16 h1:sSmVYOAHeC9doqi0gv7v86oY/BTld0SEFGaxsU9eRhE= go.etcd.io/etcd/client/v3 v3.5.16/go.mod h1:X+rExSGkyqxvu276cr2OwPLBaeqFu1cIl4vmRjAD/50= +go.etcd.io/etcd/client/v3 v3.5.21/go.mod h1:mFYy67IOqmbRf/kRUvsHixzo3iG+1OF2W2+jVIQRAnU= go.etcd.io/etcd/pkg/v3 v3.5.16 h1:cnavs5WSPWeK4TYwPYfmcr3Joz9BH+TZ6qoUtz6/+mc= go.etcd.io/etcd/pkg/v3 v3.5.16/go.mod h1:+lutCZHG5MBBFI/U4eYT5yL7sJfnexsoM20Y0t2uNuY= +go.etcd.io/etcd/pkg/v3 v3.5.21/go.mod h1:wpZx8Egv1g4y+N7JAsqi2zoUiBIUWznLjqJbylDjWgU= go.etcd.io/etcd/raft/v3 v3.5.16 h1:zBXA3ZUpYs1AwiLGPafYAKKl/CORn/uaxYDwlNwndAk= go.etcd.io/etcd/raft/v3 v3.5.16/go.mod h1:P4UP14AxofMJ/54boWilabqqWoW9eLodl6I5GdGzazI= +go.etcd.io/etcd/raft/v3 v3.5.21/go.mod h1:fmcuY5R2SNkklU4+fKVBQi2biVp5vafMrWUEj4TJ4Cs= go.etcd.io/etcd/server/v3 v3.5.16 h1:d0/SAdJ3vVsZvF8IFVb1k8zqMZ+heGcNfft71ul9GWE= go.etcd.io/etcd/server/v3 v3.5.16/go.mod h1:ynhyZZpdDp1Gq49jkUg5mfkDWZwXnn3eIqCqtJnrD/s= +go.etcd.io/etcd/server/v3 v3.5.21/go.mod h1:G1mOzdwuzKT1VRL7SqRchli/qcFrtLBTAQ4lV20sXXo= go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1 h1:A/5uWzF44DlIgdm/PQFwfMkW0JX+cIcQi/SwLAmZP5M= go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= go.opentelemetry.io/contrib/detectors/gcp v1.29.0 h1:TiaiXB4DpGD3sdzNlYQxruQngn5Apwzi1X0DRhuGvDQ= go.opentelemetry.io/contrib/detectors/gcp v1.29.0/go.mod h1:GW2aWZNwR2ZxDLdv8OyC2G8zkRoQBuURgV7RPQgcPoU= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 h1:r6I7RJCN86bpD/FQwedZ0vSixDpwuWREjW9oRMsmqDc= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0/go.mod h1:B9yO6b04uB80CzjedvewuqDhxJxi11s7/GtiGa8bAjI= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0/go.mod h1:HDBUsEjOuRC0EzKZ1bSaRGZWUBAzo+MhAcUUORSr4D0= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 h1:qFffATk0X+HD+f1Z8lswGiOQYKHRlzfmdJm0wEaVrFA= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0/go.mod h1:MOiCmryaYtc+V0Ei+Tx9o5S1ZjA7kzLucuVuyzBZloQ= go.opentelemetry.io/otel/sdk/metric v1.29.0 h1:K2CfmJohnRgvZ9UAj2/FhIf/okdWcNdBwe1m8xFXiSY= @@ -374,8 +511,8 @@ google.golang.org/api v0.215.0 h1:jdYF4qnyczlEz2ReWIsosNLDuzXyvFHJtI5gcr0J7t0= google.golang.org/api v0.215.0/go.mod h1:fta3CVtuJYOEdugLNWm6WodzOS8KdFckABwN4I40hzY= google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= -google.golang.org/genproto v0.0.0-20241118233622-e639e219e697/go.mod h1:JJrvXBWRZaFMxBufik1a4RpFw4HhgVtBBWQeQgUj2cc= gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= +gopkg.in/go-jose/go-jose.v2 v2.6.3/go.mod h1:zzZDPkNNw/c9IE7Z9jr11mBZQhKQTMzoEEIoEdZlFBI= gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI= @@ -386,21 +523,30 @@ gorgonia.org/vecf64 v0.9.0 h1:bgZDP5x0OzBF64PjMGC3EvTdOoMEcmfAh1VCUnZFm1A= gorgonia.org/vecf64 v0.9.0/go.mod h1:hp7IOWCnRiVQKON73kkC/AUMtEXyf9kGlVrtPQ9ccVA= k8s.io/code-generator v0.32.0 h1:s0lNN8VSWny8LBz5t5iy7MCdgwdOhdg7vAGVxvS+VWU= k8s.io/code-generator v0.32.0/go.mod h1:b7Q7KMZkvsYFy72A79QYjiv4aTz3GvW0f1T3UfhFq4s= +k8s.io/code-generator v0.33.3/go.mod h1:6Y02+HQJYgNphv9z3wJB5w+sjYDIEBQW7sh62PkufvA= k8s.io/component-helpers v0.32.0 h1:pQEEBmRt3pDJJX98cQvZshDgJFeKRM4YtYkMmfOlczw= k8s.io/component-helpers v0.32.0/go.mod h1:9RuClQatbClcokXOcDWSzFKQm1huIf0FzQlPRpizlMc= +k8s.io/component-helpers v0.33.3/go.mod h1:7iwv+Y9Guw6X4RrnNQOyQlXcvJrVjPveHVqUA5dm31c= k8s.io/cri-api v0.27.1 h1:KWO+U8MfI9drXB/P4oU9VchaWYOlwDglJZVHWMpTT3Q= k8s.io/cri-api v0.27.1/go.mod h1:+Ts/AVYbIo04S86XbTD73UPp/DkTiYxtsFeOFEu32L0= k8s.io/gengo/v2 v2.0.0-20240911193312-2b36238f13e9 h1:si3PfKm8dDYxgfbeA6orqrtLkvvIeH8UqffFJDl0bz4= k8s.io/gengo/v2 v2.0.0-20240911193312-2b36238f13e9/go.mod h1:EJykeLsmFC60UQbYJezXkEsG2FLrt0GPNkU5iK5GWxU= +k8s.io/gengo/v2 v2.0.0-20250207200755-1244d31929d7/go.mod h1:EJykeLsmFC60UQbYJezXkEsG2FLrt0GPNkU5iK5GWxU= k8s.io/kms v0.32.0 h1:jwOfunHIrcdYl5FRcA+uUKKtg6qiqoPCwmS2T3XTYL4= k8s.io/kms v0.32.0/go.mod h1:Bk2evz/Yvk0oVrvm4MvZbgq8BD34Ksxs2SRHn4/UiOM= +k8s.io/kms v0.33.3/go.mod h1:C1I8mjFFBNzfUZXYt9FZVJ8MJl7ynFbGgZFbBzkBJ3E= k8s.io/metrics v0.32.0 h1:70qJ3ZS/9DrtH0UA0NVBI6gW2ip2GAn9e7NtoKERpns= k8s.io/metrics v0.32.0/go.mod h1:skdg9pDjVjCPIQqmc5rBzDL4noY64ORhKu9KCPv1+QI= +k8s.io/metrics v0.33.3/go.mod h1:Aw+cdg4AYHw0HvUY+lCyq40FOO84awrqvJRTw0cmXDs= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.0 h1:CPT0ExVicCzcpeN4baWEV2ko2Z/AsiZgEdwgcfwLgMo= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.0/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= sigs.k8s.io/kustomize/kustomize/v5 v5.5.0 h1:o1mtt6vpxsxDYaZKrw3BnEtc+pAjLz7UffnIvHNbvW0= sigs.k8s.io/kustomize/kustomize/v5 v5.5.0/go.mod h1:AeFCmgCrXzmvjWWaeZCyBp6XzG1Y0w1svYus8GhJEOE= +sigs.k8s.io/kustomize/kustomize/v5 v5.6.0/go.mod h1:XuuZiQF7WdcvZzEYyNww9A0p3LazCKeJmCjeycN8e1I= tags.cncf.io/container-device-interface v0.7.2 h1:MLqGnWfOr1wB7m08ieI4YJ3IoLKKozEnnNYBtacDPQU= tags.cncf.io/container-device-interface v0.7.2/go.mod h1:Xb1PvXv2BhfNb3tla4r9JL129ck1Lxv9KuU6eVOfKto= +tags.cncf.io/container-device-interface v0.8.1/go.mod h1:Apb7N4VdILW0EVdEMRYXIDVRZfNJZ+kmEUss2kRRQ6Y= tags.cncf.io/container-device-interface/specs-go v0.7.0 h1:w/maMGVeLP6TIQJVYT5pbqTi8SCw/iHZ+n4ignuGHqg= tags.cncf.io/container-device-interface/specs-go v0.7.0/go.mod h1:hMAwAbMZyBLdmYqWgYcKH0F/yctNpV3P35f+/088A80= +tags.cncf.io/container-device-interface/specs-go v0.8.0/go.mod h1:BhJIkjjPh4qpys+qm4DAYtUyryaTDg9zris+AczXyws= diff --git a/pkg/debugcli/patch_generator.go b/pkg/debugcli/patch_generator.go index 6d4cec73..d6640f1b 100644 --- a/pkg/debugcli/patch_generator.go +++ b/pkg/debugcli/patch_generator.go @@ -10,16 +10,16 @@ import ( type PatchType string const ( - PatchTypeAddValue PatchType = "add-value" - PatchTypeChangeValue PatchType = "change-value" - PatchTypeRemoveValue PatchType = "remove-value" - PatchTypeAddBlock PatchType = "add-block" - PatchTypeRemoveBlock PatchType = "remove-block" - PatchTypeIndentation PatchType = "indentation" - PatchTypeComments PatchType = "comments" - PatchTypeReplaceBlock PatchType = "replace-block" - PatchTypeRenameKey PatchType = "rename-key" - PatchTypeSwapSections PatchType = "swap-sections" + PatchTypeAddValue PatchType = "add-value" + PatchTypeChangeValue PatchType = "change-value" + PatchTypeRemoveValue PatchType = "remove-value" + PatchTypeAddBlock PatchType = "add-block" + PatchTypeRemoveBlock PatchType = "remove-block" + PatchTypeIndentation PatchType = "indentation" + PatchTypeComments PatchType = "comments" + PatchTypeReplaceBlock PatchType = "replace-block" + PatchTypeRenameKey PatchType = "rename-key" + PatchTypeSwapSections PatchType = "swap-sections" ) // LineType categorizes what kind of line we're dealing with @@ -66,7 +66,7 @@ func NewPatchGenerator(content string) *PatchGenerator { PatchTypeRenameKey, }, } - + pg.parseLines() return pg } @@ -75,26 +75,26 @@ func NewPatchGenerator(content string) *PatchGenerator { func (pg *PatchGenerator) parseLines() { pg.parsedLines = make([]YAMLLine, 0, len(pg.lines)) parentKeyStack := []string{""} - + for i, line := range pg.lines { trimmed := strings.TrimSpace(line) indent := len(line) - len(strings.TrimLeft(line, " \t")) - + yamlLine := YAMLLine{ - Content: line, - LineNum: i, - Indent: indent, + Content: line, + LineNum: i, + Indent: indent, } - + // Track parent based on indentation for len(parentKeyStack) > 0 && (len(parentKeyStack) > 1 && indent <= pg.parsedLines[pg.getLastLineWithIndent(indent)].Indent) { parentKeyStack = parentKeyStack[:len(parentKeyStack)-1] } - + if len(parentKeyStack) > 0 { yamlLine.ParentKey = parentKeyStack[len(parentKeyStack)-1] } - + // Determine line type switch { case trimmed == "": @@ -114,7 +114,7 @@ func (pg *PatchGenerator) parseLines() { yamlLine.LineType = LineTypeKey yamlLine.Key = key yamlLine.Value = value - + // If this is a block start (ends with :), push to parent stack if value == "" || value == "{}" { yamlLine.LineType = LineTypeBlockStart @@ -125,7 +125,7 @@ func (pg *PatchGenerator) parseLines() { yamlLine.LineType = LineTypeListItem yamlLine.Value = trimmed } - + pg.parsedLines = append(pg.parsedLines, yamlLine) } } @@ -146,9 +146,9 @@ func (pg *PatchGenerator) GeneratePatch() string { for { // Select a random patch type patchType := pg.patchTypes[rand.Intn(len(pg.patchTypes))] - + var patch string - + switch patchType { case PatchTypeAddValue: patch = pg.generateAddValuePatch() @@ -166,12 +166,12 @@ func (pg *PatchGenerator) GeneratePatch() string { // If something goes wrong, fall back to adding a value patch = pg.generateAddValuePatch() } - + // Check if we have a valid patch with actual content changes (at least one + or - line) if patch != "" && containsAdditionOrDeletion(patch) { return patch } - + // If we got here, the patch wasn't valid - try again with a different type // Favor types that are more likely to generate changes if rand.Intn(100) < 70 { @@ -189,7 +189,7 @@ func containsAdditionOrDeletion(patch string) bool { // Look for lines that start with + or - but not just header lines (like +++ or ---) // This is a simple heuristic to find actual content changes if (strings.HasPrefix(line, "+") && !strings.HasPrefix(line, "+++")) || - (strings.HasPrefix(line, "-") && !strings.HasPrefix(line, "---")) { + (strings.HasPrefix(line, "-") && !strings.HasPrefix(line, "---")) { // Make sure it's not just an empty context line if len(strings.TrimSpace(line)) > 1 { return true @@ -207,25 +207,25 @@ func (pg *PatchGenerator) generateAddValuePatch() string { // Fallback to adding at root level return pg.generateRootLevelValuePatch() } - + // Pick a random block blockLine := blockLines[rand.Intn(len(blockLines))] - + // Determine the indentation level for the new value indentation := blockLine.Indent + 2 - + // Generate a random key and value key := fmt.Sprintf("new_%s_%d", getRandomItem(randomNames), rand.Intn(100)) value := generateRandomValue(key) - + // For string values, wrap in quotes if !strings.ContainsAny(value, "{}[]") && !isNumeric(value) && value != "true" && value != "false" { value = fmt.Sprintf("\"%s\"", value) } - + // Build the patch newLine := fmt.Sprintf("%s%s: %s", strings.Repeat(" ", indentation), key, value) - + // Find the last line in this block to insert after lastLineIndex := blockLine.LineNum for i := blockLine.LineNum + 1; i < len(pg.parsedLines); i++ { @@ -234,17 +234,17 @@ func (pg *PatchGenerator) generateAddValuePatch() string { } lastLineIndex = i } - + // Generate the patch var builder strings.Builder builder.WriteString("--- file\n") builder.WriteString("+++ file\n") - builder.WriteString(fmt.Sprintf("@@ -%d,%d +%d,%d @@\n", - lastLineIndex+1, 1, + builder.WriteString(fmt.Sprintf("@@ -%d,%d +%d,%d @@\n", + lastLineIndex+1, 1, lastLineIndex+1, 2)) builder.WriteString(fmt.Sprintf(" %s\n", pg.lines[lastLineIndex])) builder.WriteString(fmt.Sprintf("+%s\n", newLine)) - + return builder.String() } @@ -253,12 +253,12 @@ func (pg *PatchGenerator) generateRootLevelValuePatch() string { // Generate random key and value key := fmt.Sprintf("root_%s_%d", getRandomItem(randomNames), rand.Intn(100)) value := generateRandomValue(key) - + // For string values, wrap in quotes if !strings.ContainsAny(value, "{}[]") && !isNumeric(value) && value != "true" && value != "false" { value = fmt.Sprintf("\"%s\"", value) } - + // Find where to insert (after the last root level entry) lastRootIndex := len(pg.lines) - 1 for i := len(pg.parsedLines) - 1; i >= 0; i-- { @@ -267,17 +267,17 @@ func (pg *PatchGenerator) generateRootLevelValuePatch() string { break } } - + // Build the patch var builder strings.Builder builder.WriteString("--- file\n") builder.WriteString("+++ file\n") - builder.WriteString(fmt.Sprintf("@@ -%d,%d +%d,%d @@\n", - lastRootIndex+1, 1, + builder.WriteString(fmt.Sprintf("@@ -%d,%d +%d,%d @@\n", + lastRootIndex+1, 1, lastRootIndex+1, 2)) builder.WriteString(fmt.Sprintf(" %s\n", pg.lines[lastRootIndex])) builder.WriteString(fmt.Sprintf("+%s: %s\n", key, value)) - + return builder.String() } @@ -290,36 +290,36 @@ func (pg *PatchGenerator) generateChangeValuePatch() string { valueLines = append(valueLines, line) } } - + if len(valueLines) == 0 { // Fall back to adding a value return pg.generateAddValuePatch() } - + // Pick a random value to change valueLine := valueLines[rand.Intn(len(valueLines))] - + // Generate a new value newValue := generateRandomValue(valueLine.Key) - + // For string values, wrap in quotes if the original had them if strings.HasPrefix(valueLine.Value, "\"") && strings.HasSuffix(valueLine.Value, "\"") { newValue = fmt.Sprintf("\"%s\"", newValue) } - + // Build the new line with the same indentation newLine := fmt.Sprintf("%s%s: %s", strings.Repeat(" ", valueLine.Indent), valueLine.Key, newValue) - + // Generate the patch var builder strings.Builder builder.WriteString("--- file\n") builder.WriteString("+++ file\n") - builder.WriteString(fmt.Sprintf("@@ -%d,%d +%d,%d @@\n", - valueLine.LineNum+1, 1, + builder.WriteString(fmt.Sprintf("@@ -%d,%d +%d,%d @@\n", + valueLine.LineNum+1, 1, valueLine.LineNum+1, 1)) builder.WriteString(fmt.Sprintf("-%s\n", pg.lines[valueLine.LineNum])) builder.WriteString(fmt.Sprintf("+%s\n", newLine)) - + return builder.String() } @@ -332,58 +332,58 @@ func (pg *PatchGenerator) generateRemoveValuePatch() string { valueLines = append(valueLines, line) } } - + if len(valueLines) == 0 { // Fall back to adding a value return pg.generateAddValuePatch() } - + // Pick a random value to remove valueLine := valueLines[rand.Intn(len(valueLines))] - + // Get context lines contextBefore := "" if valueLine.LineNum > 0 { contextBefore = pg.lines[valueLine.LineNum-1] } - + contextAfter := "" if valueLine.LineNum < len(pg.lines)-1 { contextAfter = pg.lines[valueLine.LineNum+1] } - + // Generate the patch var builder strings.Builder builder.WriteString("--- file\n") builder.WriteString("+++ file\n") - + // If we have context lines, include them if contextBefore != "" && contextAfter != "" { - builder.WriteString(fmt.Sprintf("@@ -%d,%d +%d,%d @@\n", - valueLine.LineNum, 3, + builder.WriteString(fmt.Sprintf("@@ -%d,%d +%d,%d @@\n", + valueLine.LineNum, 3, valueLine.LineNum, 2)) builder.WriteString(fmt.Sprintf(" %s\n", contextBefore)) builder.WriteString(fmt.Sprintf("-%s\n", pg.lines[valueLine.LineNum])) builder.WriteString(fmt.Sprintf(" %s\n", contextAfter)) } else if contextBefore != "" { - builder.WriteString(fmt.Sprintf("@@ -%d,%d +%d,%d @@\n", - valueLine.LineNum, 2, + builder.WriteString(fmt.Sprintf("@@ -%d,%d +%d,%d @@\n", + valueLine.LineNum, 2, valueLine.LineNum, 1)) builder.WriteString(fmt.Sprintf(" %s\n", contextBefore)) builder.WriteString(fmt.Sprintf("-%s\n", pg.lines[valueLine.LineNum])) } else if contextAfter != "" { - builder.WriteString(fmt.Sprintf("@@ -%d,%d +%d,%d @@\n", - valueLine.LineNum+1, 2, + builder.WriteString(fmt.Sprintf("@@ -%d,%d +%d,%d @@\n", + valueLine.LineNum+1, 2, valueLine.LineNum+1, 1)) builder.WriteString(fmt.Sprintf("-%s\n", pg.lines[valueLine.LineNum])) builder.WriteString(fmt.Sprintf(" %s\n", contextAfter)) } else { - builder.WriteString(fmt.Sprintf("@@ -%d,%d +%d,%d @@\n", - valueLine.LineNum+1, 1, + builder.WriteString(fmt.Sprintf("@@ -%d,%d +%d,%d @@\n", + valueLine.LineNum+1, 1, valueLine.LineNum+1, 0)) builder.WriteString(fmt.Sprintf("-%s\n", pg.lines[valueLine.LineNum])) } - + return builder.String() } @@ -392,14 +392,14 @@ func (pg *PatchGenerator) generateAddBlockPatch() string { // Find a place to add a block - either at root level or in an existing block insertAfterLine := 0 indentation := 0 - + // First, try to find a block to add to blockLines := pg.findBlockStartLines() if len(blockLines) > 0 { // Pick a random block blockLine := blockLines[rand.Intn(len(blockLines))] indentation = blockLine.Indent + 2 - + // Find the last line in this block to insert after insertAfterLine = blockLine.LineNum for i := blockLine.LineNum + 1; i < len(pg.parsedLines); i++ { @@ -417,40 +417,40 @@ func (pg *PatchGenerator) generateAddBlockPatch() string { } } } - + // Generate a block name blockName := fmt.Sprintf("new_%s_%d", getRandomItem(randomNames), rand.Intn(100)) - + // Create the block with random content var blockBuilder strings.Builder blockBuilder.WriteString(fmt.Sprintf("%s%s:\n", strings.Repeat(" ", indentation), blockName)) - + // Add 2-5 fields to the block fieldCount := rand.Intn(4) + 2 for i := 0; i < fieldCount; i++ { fieldName := fmt.Sprintf("field_%d", i+1) fieldValue := generateRandomValue(fieldName) - + // For string values, wrap in quotes if !strings.ContainsAny(fieldValue, "{}[]") && !isNumeric(fieldValue) && fieldValue != "true" && fieldValue != "false" { fieldValue = fmt.Sprintf("\"%s\"", fieldValue) } - + blockBuilder.WriteString(fmt.Sprintf("%s %s: %s\n", strings.Repeat(" ", indentation), fieldName, fieldValue)) } - + blockContent := blockBuilder.String() - + // Create the patch var patchBuilder strings.Builder patchBuilder.WriteString("--- file\n") patchBuilder.WriteString("+++ file\n") - patchBuilder.WriteString(fmt.Sprintf("@@ -%d,%d +%d,%d @@\n", - insertAfterLine+1, 1, + patchBuilder.WriteString(fmt.Sprintf("@@ -%d,%d +%d,%d @@\n", + insertAfterLine+1, 1, insertAfterLine+1, 1+strings.Count(blockContent, "\n"))) patchBuilder.WriteString(fmt.Sprintf(" %s\n", pg.lines[insertAfterLine])) patchBuilder.WriteString(blockContent) - + return patchBuilder.String() } @@ -475,32 +475,32 @@ func (pg *PatchGenerator) generateAddCommentPatch() string { nonEmptyLines = append(nonEmptyLines, line) } } - + if len(nonEmptyLines) == 0 { // Fall back to adding at the beginning return pg.generateAddValuePatch() } - + // Pick a random line targetLine := nonEmptyLines[rand.Intn(len(nonEmptyLines))] - + // Generate a comment commentText := getRandomComment() indentation := targetLine.Indent - + // Build the comment line commentLine := fmt.Sprintf("%s# %s", strings.Repeat(" ", indentation), commentText) - + // Generate the patch var builder strings.Builder builder.WriteString("--- file\n") builder.WriteString("+++ file\n") - builder.WriteString(fmt.Sprintf("@@ -%d,%d +%d,%d @@\n", - targetLine.LineNum+1, 1, + builder.WriteString(fmt.Sprintf("@@ -%d,%d +%d,%d @@\n", + targetLine.LineNum+1, 1, targetLine.LineNum+1, 2)) builder.WriteString(fmt.Sprintf(" %s\n", pg.lines[targetLine.LineNum])) builder.WriteString(fmt.Sprintf("+%s\n", commentLine)) - + return builder.String() } @@ -510,27 +510,27 @@ func (pg *PatchGenerator) generateChangeCommentPatch() string { if len(commentLines) == 0 { return pg.generateAddCommentPatch() } - + // Pick a random comment commentLine := commentLines[rand.Intn(len(commentLines))] - + // Generate a new comment newCommentText := getRandomComment() indentation := commentLine.Indent - + // Build the new comment line newCommentLine := fmt.Sprintf("%s# %s", strings.Repeat(" ", indentation), newCommentText) - + // Generate the patch var builder strings.Builder builder.WriteString("--- file\n") builder.WriteString("+++ file\n") - builder.WriteString(fmt.Sprintf("@@ -%d,%d +%d,%d @@\n", - commentLine.LineNum+1, 1, + builder.WriteString(fmt.Sprintf("@@ -%d,%d +%d,%d @@\n", + commentLine.LineNum+1, 1, commentLine.LineNum+1, 1)) builder.WriteString(fmt.Sprintf("-%s\n", pg.lines[commentLine.LineNum])) builder.WriteString(fmt.Sprintf("+%s\n", newCommentLine)) - + return builder.String() } @@ -543,18 +543,18 @@ func (pg *PatchGenerator) generateRenameKeyPatch() string { keyLines = append(keyLines, line) } } - + if len(keyLines) == 0 { // Fall back to adding a value return pg.generateAddValuePatch() } - + // Pick a random key to rename keyLine := keyLines[rand.Intn(len(keyLines))] - + // Generate a new key name newKey := fmt.Sprintf("renamed_%s_%d", keyLine.Key, rand.Intn(100)) - + // Build the new line with the same indentation var newLine string if keyLine.Value == "" { @@ -564,17 +564,17 @@ func (pg *PatchGenerator) generateRenameKeyPatch() string { // This is a key-value pair newLine = fmt.Sprintf("%s%s: %s", strings.Repeat(" ", keyLine.Indent), newKey, keyLine.Value) } - + // Generate the patch var builder strings.Builder builder.WriteString("--- file\n") builder.WriteString("+++ file\n") - builder.WriteString(fmt.Sprintf("@@ -%d,%d +%d,%d @@\n", - keyLine.LineNum+1, 1, + builder.WriteString(fmt.Sprintf("@@ -%d,%d +%d,%d @@\n", + keyLine.LineNum+1, 1, keyLine.LineNum+1, 1)) builder.WriteString(fmt.Sprintf("-%s\n", pg.lines[keyLine.LineNum])) builder.WriteString(fmt.Sprintf("+%s\n", newLine)) - + return builder.String() } @@ -634,4 +634,4 @@ func getRandomComment() string { template := getRandomItem(commentTemplates) subject := getRandomItem(randomNames) return fmt.Sprintf(template, subject) -} \ No newline at end of file +} diff --git a/pkg/debugcli/yaml_generator.go b/pkg/debugcli/yaml_generator.go index 1eea9a49..6e2d1eb1 100644 --- a/pkg/debugcli/yaml_generator.go +++ b/pkg/debugcli/yaml_generator.go @@ -19,25 +19,25 @@ const ( // Random value generators var ( randomNames = []string{ - "nginx", "frontend", "backend", "database", "redis", "kafka", + "nginx", "frontend", "backend", "database", "redis", "kafka", "prometheus", "grafana", "elasticsearch", "kibana", "mongodb", "postgres", "mysql", "api", "auth", "payments", "search", "logging", "monitoring", "analytics", "cache", "queue", "worker", "scheduler", } randomImages = []string{ - "nginx:latest", "redis:6", "postgres:13", "mongo:4", + "nginx:latest", "redis:6", "postgres:13", "mongo:4", "busybox:1.35", "ubuntu:20.04", "alpine:3.15", "node:16", "python:3.9", "golang:1.17", "httpd:2.4", "rabbitmq:3", } randomPorts = []int{ - 80, 443, 8080, 8443, 3000, 3306, 5432, 6379, + 80, 443, 8080, 8443, 3000, 3306, 5432, 6379, 27017, 9090, 9200, 8000, 8888, 4000, 4040, } randomEnvVars = []string{ - "DEBUG", "LOG_LEVEL", "NODE_ENV", "ENVIRONMENT", + "DEBUG", "LOG_LEVEL", "NODE_ENV", "ENVIRONMENT", "API_KEY", "SECRET_KEY", "DATABASE_URL", "REDIS_URL", "PORT", "HOST", "TIMEOUT", "MAX_CONNECTIONS", "WORKERS", } @@ -68,11 +68,11 @@ func generateRandomValue(key string) string { return getRandomItem(randomImages) case strings.Contains(key, "port"): return fmt.Sprintf("%d", getRandomItem(randomPorts)) - case strings.Contains(key, "replica") || strings.Contains(key, "count") || - strings.Contains(key, "size") || strings.Contains(key, "limit"): - return fmt.Sprintf("%d", rand.Intn(10) + 1) - case strings.Contains(key, "enabled") || strings.Contains(key, "disabled") || - strings.Contains(key, "active"): + case strings.Contains(key, "replica") || strings.Contains(key, "count") || + strings.Contains(key, "size") || strings.Contains(key, "limit"): + return fmt.Sprintf("%d", rand.Intn(10)+1) + case strings.Contains(key, "enabled") || strings.Contains(key, "disabled") || + strings.Contains(key, "active"): return getRandomItem(randomBooleanValues) case strings.Contains(key, "version"): return fmt.Sprintf("%d.%d.%d", rand.Intn(10), rand.Intn(20), rand.Intn(100)) @@ -83,7 +83,7 @@ func generateRandomValue(key string) string { case strings.Contains(key, "annotation"): return getRandomItem(randomAnnotations) case strings.Contains(key, "field"): - // For generic fields like field_1, generate more generic values + // For generic fields like field_1, generate more generic values options := []string{"value", "setting", "config"} return fmt.Sprintf("%s-%d", getRandomItem(options), rand.Intn(1000)) default: @@ -186,7 +186,7 @@ func generateSimpleYAML(builder *strings.Builder) { func generateMediumComplexityYAML(builder *strings.Builder) { // First add the simple sections generateSimpleYAML(builder) - + // Add a persistence section builder.WriteString("\npersistence:\n") persistenceEnabled := rand.Float32() < 0.6 // 60% chance of enabling persistence @@ -197,13 +197,13 @@ func generateMediumComplexityYAML(builder *strings.Builder) { builder.WriteString(fmt.Sprintf(" mountPath: \"/data\"\n")) builder.WriteString(fmt.Sprintf(" accessMode: %s\n", getRandomItem([]string{"ReadWriteOnce", "ReadOnlyMany", "ReadWriteMany"}))) } - + // Add security context builder.WriteString("\nsecurityContext:\n") builder.WriteString(fmt.Sprintf(" runAsUser: %d\n", getRandomNumber(1000, 2000))) builder.WriteString(fmt.Sprintf(" runAsGroup: %d\n", getRandomNumber(1000, 2000))) builder.WriteString(fmt.Sprintf(" fsGroup: %d\n", getRandomNumber(1000, 2000))) - + // Add probes section builder.WriteString("\nprobes:\n") builder.WriteString(" liveness:\n") @@ -224,7 +224,7 @@ func generateMediumComplexityYAML(builder *strings.Builder) { func generateHighComplexityYAML(builder *strings.Builder) { // First add the medium complexity sections generateMediumComplexityYAML(builder) - + // Add a global section with advanced settings builder.WriteString("\nglobal:\n") builder.WriteString(" # Global settings apply to all charts\n") @@ -233,14 +233,14 @@ func generateHighComplexityYAML(builder *strings.Builder) { builder.WriteString(fmt.Sprintf(" app: %s\n", getRandomItem(randomNames))) builder.WriteString(fmt.Sprintf(" version: \"%s\"\n", fmt.Sprintf("v%d.%d.%d", getRandomNumber(0, 9), getRandomNumber(0, 99), getRandomNumber(0, 999)))) builder.WriteString(fmt.Sprintf(" managed-by: %s\n", getRandomItem([]string{"helm", "argocd", "kustomize", "flux"}))) - + // Add advanced networking builder.WriteString("\nnetworking:\n") builder.WriteString(" # Network configuration\n") builder.WriteString(" podNetworkCidr: \"10.244.0.0/16\"\n") builder.WriteString(" serviceNetworkCidr: \"10.96.0.0/12\"\n") builder.WriteString(fmt.Sprintf(" dnsPolicy: %s\n", getRandomItem([]string{"ClusterFirst", "Default", "ClusterFirstWithHostNet"}))) - + // Add monitoring integration builder.WriteString("\nmonitoring:\n") monitoringEnabled := rand.Float32() < 0.8 // 80% chance of enabling monitoring @@ -258,7 +258,7 @@ func generateHighComplexityYAML(builder *strings.Builder) { builder.WriteString(fmt.Sprintf(" enabled: %v\n", rand.Float32() < 0.7)) builder.WriteString(fmt.Sprintf(" dashboards: %v\n", rand.Float32() < 0.8)) } - + // Add autoscaling settings builder.WriteString("\nautoscaling:\n") autoscalingEnabled := rand.Float32() < 0.7 // 70% chance of enabling autoscaling @@ -273,7 +273,7 @@ func generateHighComplexityYAML(builder *strings.Builder) { builder.WriteString(" scaleUp:\n") builder.WriteString(fmt.Sprintf(" stabilizationWindowSeconds: %d\n", getRandomNumber(0, 30))) } - + // Add a detailed config map builder.WriteString("\nconfigMap:\n") builder.WriteString(" data:\n") @@ -281,7 +281,7 @@ func generateHighComplexityYAML(builder *strings.Builder) { key := fmt.Sprintf("CONFIG_%d", i+1) builder.WriteString(fmt.Sprintf(" %s: \"%s\"\n", key, fmt.Sprintf("value-%d", getRandomNumber(100, 999)))) } - + // Add multi-container pod settings builder.WriteString("\nsidecars:\n") for i := 0; i < getRandomNumber(1, 3); i++ { @@ -296,4 +296,4 @@ func generateHighComplexityYAML(builder *strings.Builder) { builder.WriteString(fmt.Sprintf(" cpu: %dm\n", getRandomNumber(10, 100))) builder.WriteString(fmt.Sprintf(" memory: %dMi\n", getRandomNumber(32, 128))) } -} \ No newline at end of file +} diff --git a/pkg/diff/apply.go b/pkg/diff/apply.go index f227f02e..c244afa4 100644 --- a/pkg/diff/apply.go +++ b/pkg/diff/apply.go @@ -41,9 +41,9 @@ func ApplyPatch(content string, patchText string) (string, error) { patchLines := strings.Split(patchText, "\n") if len(patchLines) < 3 { // Handle trivial cases for very simple patches - if len(patchLines) == 1 && (strings.HasPrefix(patchLines[0], "+") || - strings.HasPrefix(patchLines[0], "-") || - strings.HasPrefix(patchLines[0], " ")) { + if len(patchLines) == 1 && (strings.HasPrefix(patchLines[0], "+") || + strings.HasPrefix(patchLines[0], "-") || + strings.HasPrefix(patchLines[0], " ")) { // Handle as a simple single-line patch if strings.HasPrefix(patchLines[0], "+") { return content + strings.TrimPrefix(patchLines[0], "+") + "\n", nil @@ -70,10 +70,10 @@ func ApplyPatch(content string, patchText string) (string, error) { // Parse the content into lines contentLines := strings.Split(content, "\n") - + // Prepare the processing structures result := applyHunksToContent(contentLines, hunks) - + // Join the resulting lines resultText := strings.Join(result, "\n") @@ -81,19 +81,19 @@ func ApplyPatch(content string, patchText string) (string, error) { if strings.HasSuffix(content, "\n") && !strings.HasSuffix(resultText, "\n") { resultText += "\n" } - + return resultText, nil } // extractHunks parses a patch and extracts all hunks func extractHunks(patchText string) ([]hunk, error) { patchLines := strings.Split(patchText, "\n") - + // First approach: use our own parser to extract hunks var hunks []hunk var currentHunk *hunk var hunkStarted bool = false - + // Look for hunk headers (@@) and build hunks for _, line := range patchLines { if strings.HasPrefix(line, "@@") { @@ -101,7 +101,7 @@ func extractHunks(patchText string) ([]hunk, error) { if currentHunk != nil { hunks = append(hunks, *currentHunk) } - + // Parse the hunk header h := &hunk{ header: line, @@ -110,7 +110,7 @@ func extractHunks(patchText string) ([]hunk, error) { removedLines: []string{}, addedLines: []string{}, } - + // Parse line numbers from the header re := regexp.MustCompile(`@@ -(\d+),(\d+) \+(\d+),(\d+) @@`) matches := re.FindStringSubmatch(line) @@ -126,18 +126,18 @@ func extractHunks(patchText string) ([]hunk, error) { if len(parts) >= 3 { original := strings.TrimPrefix(parts[1], "-") modified := strings.TrimPrefix(parts[2], "+") - + h.originalStart, h.originalCount = parseHunkRange(original) h.modifiedStart, h.modifiedCount = parseHunkRange(modified) hunkStarted = true } } - + currentHunk = h } else if currentHunk != nil && !strings.HasPrefix(line, "---") && !strings.HasPrefix(line, "+++") { // Add line to current hunk currentHunk.content = append(currentHunk.content, line) - + // Categorize line by type if strings.HasPrefix(line, " ") { currentHunk.contextLines = append(currentHunk.contextLines, strings.TrimPrefix(line, " ")) @@ -148,12 +148,12 @@ func extractHunks(patchText string) ([]hunk, error) { } } } - + // Add the last hunk if there is one if currentHunk != nil { hunks = append(hunks, *currentHunk) } - + // If no hunks found with our parser, fall back to the reconstructor if len(hunks) == 0 || !hunkStarted { reconstructor := NewDiffReconstructor("", patchText) @@ -163,35 +163,35 @@ func extractHunks(patchText string) ([]hunk, error) { return nil, fmt.Errorf("failed to parse hunks: %w", err) } } - + return hunks, nil } // applyHunksToContent applies hunks to the content lines and returns the result func applyHunksToContent(contentLines []string, hunks []hunk) []string { result := make([]string, 0, len(contentLines)) - + // Track the position in the original content linePos := 0 - + // Process each hunk in order for _, hunk := range hunks { // Skip invalid hunks if hunk.originalStart < 1 || hunk.modifiedStart < 1 { continue } - + // Add lines before the hunk for linePos < hunk.originalStart-1 && linePos < len(contentLines) { result = append(result, contentLines[linePos]) linePos++ } - + // Process the hunk content hunkPos := 0 for hunkPos < len(hunk.content) { line := hunk.content[hunkPos] - + if strings.HasPrefix(line, " ") { // Context line - include it if linePos < len(contentLines) { @@ -215,12 +215,12 @@ func applyHunksToContent(contentLines []string, hunks []hunk) []string { } } } - + // Add any remaining lines after the last hunk for linePos < len(contentLines) { result = append(result, contentLines[linePos]) linePos++ } - + return result } diff --git a/pkg/diff/apply_test.go b/pkg/diff/apply_test.go index 17cc0d82..9a4aa9fa 100644 --- a/pkg/diff/apply_test.go +++ b/pkg/diff/apply_test.go @@ -66,7 +66,7 @@ func TestApplyPatch(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := ApplyPatch(tt.content, tt.patch) - + // Check error expectation if tt.expectError && err == nil { t.Error("expected an error but got none") @@ -76,7 +76,7 @@ func TestApplyPatch(t *testing.T) { t.Errorf("unexpected error: %v", err) return } - + // Skip comparison if we expected an error if tt.expectError { return @@ -119,9 +119,9 @@ func TestApplyPatches(t *testing.T) { expectError: false, }, { - name: "empty patches", - content: "test content\n", - patches: []string{"", " ", "\n"}, + name: "empty patches", + content: "test content\n", + patches: []string{"", " ", "\n"}, expected: "test content\n", expectError: false, }, @@ -140,7 +140,7 @@ func TestApplyPatches(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := ApplyPatches(tt.content, tt.patches) - + // Check error expectation if tt.expectError && err == nil { t.Error("expected an error but got none") @@ -150,7 +150,7 @@ func TestApplyPatches(t *testing.T) { t.Errorf("unexpected error: %v", err) return } - + // Skip comparison if we expected an error if tt.expectError { return @@ -162,4 +162,4 @@ func TestApplyPatches(t *testing.T) { } }) } -} \ No newline at end of file +} diff --git a/pkg/diff/reconstruct.go b/pkg/diff/reconstruct.go index e1175c0c..e3c343b6 100644 --- a/pkg/diff/reconstruct.go +++ b/pkg/diff/reconstruct.go @@ -17,9 +17,9 @@ type hunk struct { modifiedCount int contextBefore []string contextAfter []string - contextLines []string // Lines with space prefix - used for fuzzy matching - removedLines []string // Lines with - prefix - addedLines []string // Lines with + prefix + contextLines []string // Lines with space prefix - used for fuzzy matching + removedLines []string // Lines with - prefix + addedLines []string // Lines with + prefix } type DiffReconstructor struct { @@ -63,7 +63,7 @@ func (d *DiffReconstructor) ReconstructDiff() (string, error) { // Find first valid header pair origFile := "file" startIdx := 0 - + // Look for standard headers foundHeaders, of, _, si := d.findFirstValidHeaders(lines) if foundHeaders { @@ -77,7 +77,7 @@ func (d *DiffReconstructor) ReconstructDiff() (string, error) { break } } - + if hunkIdx >= 0 { startIdx = hunkIdx } @@ -133,9 +133,9 @@ func (d *DiffReconstructor) ReconstructDiff() (string, error) { // Write hunks with corrected line numbers for _, h := range correctedHunks { // Generate updated header with corrected line numbers - result.WriteString(fmt.Sprintf("@@ -%d,%d +%d,%d @@\n", + result.WriteString(fmt.Sprintf("@@ -%d,%d +%d,%d @@\n", h.originalStart, h.originalCount, h.modifiedStart, h.modifiedCount)) - + for _, line := range h.content { result.WriteString(line + "\n") } @@ -160,7 +160,7 @@ func (d *DiffReconstructor) findFirstValidHeaders(lines []string) (bool, string, func (d *DiffReconstructor) parseHunks(lines []string) ([]hunk, error) { var hunks []hunk var currentHunk *hunk - + // Handle special case for missing header format like "@@" without line numbers if len(lines) > 0 && lines[0] == "@@" { // Initialize a hunk with default values @@ -175,13 +175,13 @@ func (d *DiffReconstructor) parseHunks(lines []string) ([]hunk, error) { removedLines: []string{}, addedLines: []string{}, } - + // Process the remaining lines for i := 1; i < len(lines); i++ { line := strings.TrimRight(lines[i], "\r\n") if !strings.HasPrefix(line, "---") && !strings.HasPrefix(line, "+++") { currentHunk.content = append(currentHunk.content, line) - + // Categorize by line type for later analysis if strings.HasPrefix(line, " ") { currentHunk.contextLines = append(currentHunk.contextLines, strings.TrimPrefix(line, " ")) @@ -192,22 +192,22 @@ func (d *DiffReconstructor) parseHunks(lines []string) ([]hunk, error) { } } } - + // Add the hunk and return if len(currentHunk.content) > 0 { // Count the number of lines addCount := len(currentHunk.addedLines) removeCount := len(currentHunk.removedLines) contextCount := len(currentHunk.contextLines) - + currentHunk.originalCount = removeCount + contextCount currentHunk.modifiedCount = addCount + contextCount - + hunks = append(hunks, *currentHunk) return hunks, nil } } - + // Standard parsing for normal hunks for i := 0; i < len(lines); i++ { line := strings.TrimRight(lines[i], "\r\n") @@ -231,7 +231,7 @@ func (d *DiffReconstructor) parseHunks(lines []string) ([]hunk, error) { if parts := strings.Split(line, " "); len(parts) >= 3 { original := strings.TrimPrefix(parts[1], "-") modified := strings.TrimPrefix(parts[2], "+") - + h.originalStart, h.originalCount = parseHunkRange(original) h.modifiedStart, h.modifiedCount = parseHunkRange(modified) } else { @@ -241,7 +241,7 @@ func (d *DiffReconstructor) parseHunks(lines []string) ([]hunk, error) { h.modifiedStart = 1 h.modifiedCount = 1 } - + currentHunk = h continue } @@ -249,7 +249,7 @@ func (d *DiffReconstructor) parseHunks(lines []string) ([]hunk, error) { if currentHunk != nil && !strings.HasPrefix(line, "---") && !strings.HasPrefix(line, "+++") { // Add line to current hunk content currentHunk.content = append(currentHunk.content, line) - + // Also categorize by line type for later analysis if strings.HasPrefix(line, " ") { currentHunk.contextLines = append(currentHunk.contextLines, strings.TrimPrefix(line, " ")) @@ -258,8 +258,8 @@ func (d *DiffReconstructor) parseHunks(lines []string) ([]hunk, error) { } else if strings.HasPrefix(line, "+") { currentHunk.addedLines = append(currentHunk.addedLines, strings.TrimPrefix(line, "+")) } - } else if currentHunk == nil && (strings.HasPrefix(line, " ") || - strings.HasPrefix(line, "+") || strings.HasPrefix(line, "-")) { + } else if currentHunk == nil && (strings.HasPrefix(line, " ") || + strings.HasPrefix(line, "+") || strings.HasPrefix(line, "-")) { // Handle case where there is no explicit hunk marker but content looks like a diff currentHunk = &hunk{ header: "@@", @@ -272,10 +272,10 @@ func (d *DiffReconstructor) parseHunks(lines []string) ([]hunk, error) { removedLines: []string{}, addedLines: []string{}, } - + // Add this line to the content currentHunk.content = append(currentHunk.content, line) - + // Categorize by line type if strings.HasPrefix(line, " ") { currentHunk.contextLines = append(currentHunk.contextLines, strings.TrimPrefix(line, " ")) @@ -292,13 +292,13 @@ func (d *DiffReconstructor) parseHunks(lines []string) ([]hunk, error) { addCount := len(currentHunk.addedLines) removeCount := len(currentHunk.removedLines) contextCount := len(currentHunk.contextLines) - + // Update line counts if this was an inferred hunk if currentHunk.header == "@@" { currentHunk.originalCount = removeCount + contextCount currentHunk.modifiedCount = addCount + contextCount } - + hunks = append(hunks, *currentHunk) } @@ -307,14 +307,14 @@ func (d *DiffReconstructor) parseHunks(lines []string) ([]hunk, error) { func (d *DiffReconstructor) enhanceHunks(hunks []hunk) ([]hunk, error) { enhancedHunks := make([]hunk, len(hunks)) - + for i, h := range hunks { enhancedHunk := h - + // Extract context before and after the changes var contextBefore, contextAfter []string inChanges := false - + for _, line := range h.content { if strings.HasPrefix(line, "-") || strings.HasPrefix(line, "+") { inChanges = true @@ -326,64 +326,64 @@ func (d *DiffReconstructor) enhanceHunks(hunks []hunk) ([]hunk, error) { } } } - + enhancedHunk.contextBefore = contextBefore enhancedHunk.contextAfter = contextAfter - + // Preserve original content exactly, including all whitespace // This avoids mangling indentation in the patches enhancedHunks[i] = enhancedHunk } - + return enhancedHunks, nil } func (d *DiffReconstructor) findHunkPositions(hunks []hunk) ([]hunk, error) { originalLines := strings.Split(d.originalContent, "\n") correctedHunks := make([]hunk, 0, len(hunks)) - + for _, h := range hunks { correctedHunk := h - + // If the hunk has no context lines, try to use the removed lines for matching effectiveContext := h.contextLines if len(effectiveContext) == 0 && len(h.removedLines) > 0 { effectiveContext = h.removedLines } - + // If we still have no context, use the original start position if len(effectiveContext) == 0 { // Keep original position correctedHunks = append(correctedHunks, correctedHunk) continue } - + // Try to find the best position for this hunk using fuzzy matching bestPos, score := d.findBestMatchForHunk(originalLines, effectiveContext) - d.logDebug("Best match for hunk at original pos %d: new pos %d with score %.2f", + d.logDebug("Best match for hunk at original pos %d: new pos %d with score %.2f", h.originalStart, bestPos, score) - + if score > 0.6 && bestPos > 0 { // Only adjust if we have a good match // Adjust the line numbers deltaPos := bestPos - h.originalStart correctedHunk.originalStart = bestPos correctedHunk.modifiedStart = h.modifiedStart + deltaPos - + // Adjust content indentation if needed correctedHunk.content = d.adjustIndentation(correctedHunk.content, originalLines, bestPos) - + correctedHunks = append(correctedHunks, correctedHunk) } else { // Keep original position if no good match found correctedHunks = append(correctedHunks, correctedHunk) } } - + // Sort hunks by position to ensure proper ordering sort.Slice(correctedHunks, func(i, j int) bool { return correctedHunks[i].originalStart < correctedHunks[j].originalStart }) - + return correctedHunks, nil } @@ -391,15 +391,15 @@ func (d *DiffReconstructor) findBestMatchForHunk(originalLines []string, context if len(contextLines) == 0 || len(originalLines) == 0 { return 1, 0 // No context to match } - + bestPos := 1 bestScore := 0.0 - + // Try to match each potential position - for pos := 1; pos <= len(originalLines) - len(contextLines) + 1; pos++ { + for pos := 1; pos <= len(originalLines)-len(contextLines)+1; pos++ { score := 0.0 matchCount := 0 - + // Calculate how well context lines match at this position for i, contextLine := range contextLines { if pos+i-1 < len(originalLines) { @@ -411,21 +411,21 @@ func (d *DiffReconstructor) findBestMatchForHunk(originalLines []string, context } } } - + // Normalize score avgScore := score / float64(len(contextLines)) - + // Bonus for consecutive matches if matchCount > 2 { avgScore += 0.1 * float64(matchCount) / float64(len(contextLines)) } - + if avgScore > bestScore { bestScore = avgScore bestPos = pos } } - + return bestPos, bestScore } @@ -433,50 +433,50 @@ func (d *DiffReconstructor) adjustIndentation(lines []string, originalLines []st // For whitespace-sensitive issues, just preserve the original lines // This is a more conservative approach that prevents whitespace mangling return lines - + // Below is the original, more aggressive whitespace-fixing approach // Left commented for future reference if needed /* - adjustedLines := make([]string, len(lines)) - linePos := startPos - 1 // Convert to 0-based indexing - - for i, line := range lines { - if strings.HasPrefix(line, " ") { - // Context line - adjust indentation based on original - content := strings.TrimPrefix(line, " ") - if linePos >= 0 && linePos < len(originalLines) { - // Get indentation from original file - origIndent := extractIndentation(originalLines[linePos]) - adjustedLines[i] = " " + origIndent + strings.TrimLeft(content, " \t") - linePos++ - } else { - adjustedLines[i] = line // Keep as-is - } - } else if strings.HasPrefix(line, "-") { - // Removed line - also adjust indentation - content := strings.TrimPrefix(line, "-") - if linePos >= 0 && linePos < len(originalLines) { - origIndent := extractIndentation(originalLines[linePos]) - adjustedLines[i] = "-" + origIndent + strings.TrimLeft(content, " \t") - linePos++ + adjustedLines := make([]string, len(lines)) + linePos := startPos - 1 // Convert to 0-based indexing + + for i, line := range lines { + if strings.HasPrefix(line, " ") { + // Context line - adjust indentation based on original + content := strings.TrimPrefix(line, " ") + if linePos >= 0 && linePos < len(originalLines) { + // Get indentation from original file + origIndent := extractIndentation(originalLines[linePos]) + adjustedLines[i] = " " + origIndent + strings.TrimLeft(content, " \t") + linePos++ + } else { + adjustedLines[i] = line // Keep as-is + } + } else if strings.HasPrefix(line, "-") { + // Removed line - also adjust indentation + content := strings.TrimPrefix(line, "-") + if linePos >= 0 && linePos < len(originalLines) { + origIndent := extractIndentation(originalLines[linePos]) + adjustedLines[i] = "-" + origIndent + strings.TrimLeft(content, " \t") + linePos++ + } else { + adjustedLines[i] = line + } + } else if strings.HasPrefix(line, "+") { + // Added line - try to use same indentation as surrounding context + content := strings.TrimPrefix(line, "+") + // Use same indentation as previous line if possible + prevIndent := "" + if i > 0 && (strings.HasPrefix(lines[i-1], " ") || strings.HasPrefix(lines[i-1], "-")) { + prevIndent = extractIndentation(strings.TrimPrefix(strings.TrimPrefix(lines[i-1], " "), "-")) + } + adjustedLines[i] = "+" + prevIndent + strings.TrimLeft(content, " \t") } else { - adjustedLines[i] = line - } - } else if strings.HasPrefix(line, "+") { - // Added line - try to use same indentation as surrounding context - content := strings.TrimPrefix(line, "+") - // Use same indentation as previous line if possible - prevIndent := "" - if i > 0 && (strings.HasPrefix(lines[i-1], " ") || strings.HasPrefix(lines[i-1], "-")) { - prevIndent = extractIndentation(strings.TrimPrefix(strings.TrimPrefix(lines[i-1], " "), "-")) + adjustedLines[i] = line // Keep other lines as-is } - adjustedLines[i] = "+" + prevIndent + strings.TrimLeft(content, " \t") - } else { - adjustedLines[i] = line // Keep other lines as-is } - } - - return adjustedLines + + return adjustedLines */ } @@ -506,7 +506,7 @@ func calculateStringSimilarity(a, b string) float64 { // Normalize strings by trimming leading/trailing whitespace and converting to lowercase aNorm := strings.TrimSpace(a) bNorm := strings.TrimSpace(b) - + // For empty strings if len(aNorm) == 0 && len(bNorm) == 0 { return 1.0 @@ -514,18 +514,18 @@ func calculateStringSimilarity(a, b string) float64 { if len(aNorm) == 0 || len(bNorm) == 0 { return 0.0 } - + // Different similarity measures: - + // 1. Simple character by character matching charMatch := calculateCharacterMatch(aNorm, bNorm) - + // 2. Token-based similarity (splits by whitespace and compares tokens) tokenMatch := calculateTokenMatch(aNorm, bNorm) - + // 3. Indentation-aware comparison (ignore indentation differences) indentationMatch := calculateIndentationAwareMatch(a, b) - + // Combine scores giving priority to token and indentation matching return 0.2*charMatch + 0.4*tokenMatch + 0.4*indentationMatch } @@ -536,7 +536,7 @@ func calculateCharacterMatch(a, b string) float64 { if maxLen == 0 { return 1.0 } - + // Count matching characters matchCount := 0 for i := 0; i < min(len(a), len(b)); i++ { @@ -544,7 +544,7 @@ func calculateCharacterMatch(a, b string) float64 { matchCount++ } } - + return float64(matchCount) / float64(maxLen) } @@ -552,14 +552,14 @@ func calculateCharacterMatch(a, b string) float64 { func calculateTokenMatch(a, b string) float64 { aTokens := strings.Fields(a) bTokens := strings.Fields(b) - + if len(aTokens) == 0 && len(bTokens) == 0 { return 1.0 } if len(aTokens) == 0 || len(bTokens) == 0 { return 0.0 } - + // Count matching tokens matches := 0 for _, at := range aTokens { @@ -570,7 +570,7 @@ func calculateTokenMatch(a, b string) float64 { } } } - + // Return match ratio return float64(matches) / float64(max(len(aTokens), len(bTokens))) } @@ -580,14 +580,14 @@ func calculateIndentationAwareMatch(a, b string) float64 { // Strip all whitespace and compare aStripped := regexp.MustCompile(`\s+`).ReplaceAllString(a, "") bStripped := regexp.MustCompile(`\s+`).ReplaceAllString(b, "") - + if len(aStripped) == 0 && len(bStripped) == 0 { return 1.0 } if len(aStripped) == 0 || len(bStripped) == 0 { return 0.0 } - + // Calculate exact match percentage matchCount := 0 for i := 0; i < min(len(aStripped), len(bStripped)); i++ { @@ -595,7 +595,7 @@ func calculateIndentationAwareMatch(a, b string) float64 { matchCount++ } } - + return float64(matchCount) / float64(max(len(aStripped), len(bStripped))) } diff --git a/pkg/diff/reconstruct_advanced_test.go b/pkg/diff/reconstruct_advanced_test.go index 48d563bd..5786dba2 100644 --- a/pkg/diff/reconstruct_advanced_test.go +++ b/pkg/diff/reconstruct_advanced_test.go @@ -421,4 +421,4 @@ services: } }) } -} \ No newline at end of file +} diff --git a/pkg/listener/execute-plan.go b/pkg/listener/execute-plan.go index 1d9f6fb5..04a2c789 100644 --- a/pkg/listener/execute-plan.go +++ b/pkg/listener/execute-plan.go @@ -173,4 +173,4 @@ func handleExecutePlanNotification(ctx context.Context, payload string) error { } return nil -} \ No newline at end of file +} diff --git a/pkg/listener/heartbeat.go b/pkg/listener/heartbeat.go index ca6ef8aa..1c1bd3e7 100644 --- a/pkg/listener/heartbeat.go +++ b/pkg/listener/heartbeat.go @@ -71,4 +71,4 @@ func ensureActiveConnection(ctx context.Context) error { } return nil -} \ No newline at end of file +} diff --git a/pkg/listener/listener.go b/pkg/listener/listener.go index eb3d00b7..50985743 100644 --- a/pkg/listener/listener.go +++ b/pkg/listener/listener.go @@ -202,7 +202,7 @@ func (l *Listener) processNotifications(ctx context.Context) { // Use a dedicated connection for health checks healthCtx, healthCancel := context.WithTimeout(ctx, 5*time.Second) - + // Create a new connection just for this health check healthConn, err := pgx.Connect(healthCtx, l.pgURI) if err != nil { @@ -210,12 +210,12 @@ func (l *Listener) processNotifications(ctx context.Context) { healthCancel() continue } - + var result int err = healthConn.QueryRow(healthCtx, "SELECT 1").Scan(&result) - + // Always close the health check connection - healthConn.Close(healthCtx) + healthConn.Close(healthCtx) healthCancel() if err != nil { @@ -381,7 +381,7 @@ func (l *Listener) processQueue(ctx context.Context, processor *queueProcessor) // Create a context with timeout for database operations dbCtx, dbCancel := context.WithTimeout(ctx, 10*time.Second) - + // PHASE 1: Get queue statistics with a dedicated connection statsConn, err := pgx.Connect(dbCtx, l.pgURI) if err != nil { @@ -400,7 +400,7 @@ func (l *Listener) processQueue(ctx context.Context, processor *queueProcessor) FROM %s WHERE channel = $1 AND completed_at IS NULL`, WorkQueueTable), processor.channel).Scan(&total, &inFlight, &available) - + // Always close the connection when done statsConn.Close(dbCtx) @@ -480,7 +480,7 @@ func (l *Listener) processQueue(ctx context.Context, processor *queueProcessor) messages = append(messages, msg) } rows.Close() - + // Close the fetch connection as soon as we're done with it fetchConn.Close(dbCtx) dbCancel() @@ -540,7 +540,7 @@ func (l *Listener) processQueue(ctx context.Context, processor *queueProcessor) // Create a new context with timeout for database operations updateCtx, updateCancel := context.WithTimeout(ctx, 10*time.Second) - + // Use a new pooled connection for updating the message status updateConn, connErr := pgx.Connect(updateCtx, l.pgURI) if connErr != nil { @@ -548,9 +548,9 @@ func (l *Listener) processQueue(ctx context.Context, processor *queueProcessor) updateCancel() return } - + var dbErr error - + if handlerErr != nil { // If processing failed, mark it as available for retry _, dbErr = updateConn.Exec(updateCtx, fmt.Sprintf(` @@ -573,11 +573,11 @@ func (l *Listener) processQueue(ctx context.Context, processor *queueProcessor) logger.Error(fmt.Errorf("failed to mark message %s as completed: %w", messageID, dbErr)) } } - + // Always clean up the database connection updateConn.Close(updateCtx) updateCancel() - + if handlerErr != nil || dbErr != nil { return } @@ -775,4 +775,4 @@ func (l *Listener) Stop(ctx context.Context) error { return l.conn.Close(ctx) } return nil -} \ No newline at end of file +} diff --git a/pkg/listener/start.go b/pkg/listener/start.go index dbfa8d14..09f9fd9f 100644 --- a/pkg/listener/start.go +++ b/pkg/listener/start.go @@ -136,4 +136,4 @@ func handleConversionNextFileNotificationWithLock(ctx context.Context, payload s func handleConversionSimplifyNotificationWithLock(ctx context.Context, payload string) error { return handleConversionSimplifyNotification(ctx, payload) -} \ No newline at end of file +} diff --git a/pkg/llm/execute-action.go b/pkg/llm/execute-action.go index 5e6e805f..27eed952 100644 --- a/pkg/llm/execute-action.go +++ b/pkg/llm/execute-action.go @@ -239,23 +239,23 @@ func PerformStringReplacement(content, oldStr, newStr string) (string, bool, err // Add logging to track performance startTime := time.Now() defer func() { - logger.Debug("String replacement operation completed", + logger.Debug("String replacement operation completed", zap.Duration("time_taken", time.Since(startTime))) }() - + // Log content sizes for diagnostics - logger.Debug("Starting string replacement", - zap.Int("content_size", len(content)), + logger.Debug("Starting string replacement", + zap.Int("content_size", len(content)), zap.Int("old_string_size", len(oldStr)), zap.Int("new_string_size", len(newStr))) - + // First try exact match if strings.Contains(content, oldStr) { logger.Debug("Found exact match, performing replacement") updatedContent := strings.ReplaceAll(content, oldStr, newStr) return updatedContent, true, nil } - + logger.Debug("No exact match found, attempting fuzzy matching") // Create a context with timeout for fuzzy matching @@ -272,14 +272,14 @@ func PerformStringReplacement(content, oldStr, newStr string) (string, bool, err go func() { logger.Debug("Starting fuzzy match search") fuzzyStartTime := time.Now() - + start, end := findBestMatchRegion(content, oldStr, minFuzzyMatchLen) - - logger.Debug("Fuzzy match search completed", + + logger.Debug("Fuzzy match search completed", zap.Duration("time_taken", time.Since(fuzzyStartTime)), zap.Int("start_pos", start), zap.Int("end_pos", end)) - + if start == -1 || end == -1 { resultCh <- struct { start, end int @@ -301,15 +301,15 @@ func PerformStringReplacement(content, oldStr, newStr string) (string, bool, err return content, false, result.err } // Replace the matched region with newStr - logger.Debug("Found fuzzy match, performing replacement", - zap.Int("match_start", result.start), + logger.Debug("Found fuzzy match, performing replacement", + zap.Int("match_start", result.start), zap.Int("match_end", result.end), - zap.Int("match_length", result.end - result.start)) - + zap.Int("match_length", result.end-result.start)) + updatedContent := content[:result.start] + newStr + content[result.end:] return updatedContent, false, nil case <-ctx.Done(): - logger.Warn("Fuzzy matching timed out", + logger.Warn("Fuzzy matching timed out", zap.Duration("timeout", fuzzyMatchTimeout), zap.Duration("time_elapsed", time.Since(startTime))) return content, false, fmt.Errorf("fuzzy matching timed out after %v", fuzzyMatchTimeout) @@ -319,8 +319,8 @@ func PerformStringReplacement(content, oldStr, newStr string) (string, bool, err func findBestMatchRegion(content, oldStr string, minMatchLen int) (int, int) { // Early return if strings are too small if len(oldStr) < minMatchLen { - logger.Debug("String too small for fuzzy matching", - zap.Int("length", len(oldStr)), + logger.Debug("String too small for fuzzy matching", + zap.Int("length", len(oldStr)), zap.Int("min_length", minMatchLen)) return -1, -1 } @@ -328,7 +328,7 @@ func findBestMatchRegion(content, oldStr string, minMatchLen int) (int, int) { bestStart := -1 bestEnd := -1 bestLen := 0 - + // Set a max number of chunks to process to prevent excessive computation maxChunks := 100 chunksProcessed := 0 @@ -344,32 +344,32 @@ func findBestMatchRegion(content, oldStr string, minMatchLen int) (int, int) { // Get the current chunk chunk := oldStr[i:chunkEnd] - + // Skip empty or tiny chunks if len(chunk) < 10 { continue } - + chunksProcessed++ - + // Find all occurrences of this chunk in the content start := 0 - maxOccurrences := 100 // Limit number of occurrences to check + maxOccurrences := 100 // Limit number of occurrences to check occurrencesChecked := 0 - - logger.Debug("Processing chunk", - zap.Int("chunk_index", i), + + logger.Debug("Processing chunk", + zap.Int("chunk_index", i), zap.Int("chunk_size", len(chunk)), zap.Int("chunks_processed", chunksProcessed)) - + for occurrencesChecked < maxOccurrences { idx := strings.Index(content[start:], chunk) if idx == -1 { break } - + occurrencesChecked++ - + // Adjust index to be relative to the start of content idx += start @@ -377,13 +377,13 @@ func findBestMatchRegion(content, oldStr string, minMatchLen int) (int, int) { matchStart := idx matchEnd := idx + len(chunk) matchLen := len(chunk) - + // Store the original i value, we'll need it for backward extension originalI := i // Try to extend forward - for matchEnd < len(content) && (i+matchLen) < len(oldStr) { - if content[matchEnd] == oldStr[i+matchLen] { + for matchEnd < len(content) && (originalI+matchLen) < len(oldStr) { + if content[matchEnd] == oldStr[originalI+matchLen] { matchEnd++ matchLen++ } else { @@ -393,7 +393,7 @@ func findBestMatchRegion(content, oldStr string, minMatchLen int) (int, int) { // Try to extend backward // Critical fix: don't modify the outer loop variable i here - backPos := originalI - 1 // Start one position before chunk + backPos := originalI - 1 // Start one position before chunk for matchStart > 0 && backPos >= 0 { if content[matchStart-1] == oldStr[backPos] { matchStart-- @@ -408,8 +408,8 @@ func findBestMatchRegion(content, oldStr string, minMatchLen int) (int, int) { bestStart = matchStart bestEnd = matchEnd bestLen = matchLen - - logger.Debug("Found better match", + + logger.Debug("Found better match", zap.Int("match_length", matchLen), zap.Int("match_start", matchStart), zap.Int("match_end", matchEnd)) @@ -421,13 +421,13 @@ func findBestMatchRegion(content, oldStr string, minMatchLen int) (int, int) { } if bestLen >= minMatchLen { - logger.Debug("Found best match", + logger.Debug("Found best match", zap.Int("best_length", bestLen), zap.Int("best_start", bestStart), zap.Int("best_end", bestEnd)) return bestStart, bestEnd } - + logger.Debug("No match found with minimum length", zap.Int("best_length", bestLen), zap.Int("required_min_length", minMatchLen)) @@ -458,7 +458,7 @@ func ExecuteAction(ctx context.Context, actionPlanWithPath llmtypes.ActionPlanWi // Send error to the error channel and exit select { - case errCh <- fmt.Errorf(errMsg): + case errCh <- fmt.Errorf("No activity from LLM for 2 minutes, operation stalled (last activity at %s)", lastActivity.Format(time.RFC3339)): default: // Channel already has an error } diff --git a/pkg/persistence/pg.go b/pkg/persistence/pg.go index 659dd66c..64e12ae4 100644 --- a/pkg/persistence/pg.go +++ b/pkg/persistence/pg.go @@ -37,7 +37,7 @@ func InitPostgres(opts PostgresOpts) error { if err != nil { return fmt.Errorf("failed to parse Postgres URI: %w", err) } - + // Increase max connections in the pool poolConfig.MaxConns = 30 // Set reasonable connection lifetime to prevent stale connections @@ -46,8 +46,8 @@ func InitPostgres(opts PostgresOpts) error { poolConfig.MaxConnIdleTime = 15 * time.Minute // Set health check interval poolConfig.HealthCheckPeriod = 1 * time.Minute - - logger.Info("Initializing database connection pool", + + logger.Info("Initializing database connection pool", zap.Int32("MaxConns", poolConfig.MaxConns), zap.Duration("MaxConnLifetime", poolConfig.MaxConnLifetime), zap.Duration("MaxConnIdleTime", poolConfig.MaxConnIdleTime)) @@ -56,7 +56,7 @@ func InitPostgres(opts PostgresOpts) error { if err != nil { return fmt.Errorf("failed to create Postgres pool: %w", err) } - + // Start a background goroutine to monitor pool health and log stats periodically go monitorPoolHealth() @@ -90,7 +90,7 @@ func MustGetPooledPostgresSession() *pgxpool.Conn { zap.Int32("IdleConns", pool.Stat().IdleConns()), zap.Int32("MaxConns", pool.Stat().MaxConns())) } - + // If the pool is saturated, log a warning if pool.Stat().AcquiredConns() >= pool.Stat().MaxConns() { logger.Warn("WARNING: Connection pool saturated", @@ -101,46 +101,46 @@ func MustGetPooledPostgresSession() *pgxpool.Conn { // Track timing for connection acquisition startTime := time.Now() - + // Try 3 times to get a connection with increasing timeouts var conn *pgxpool.Conn var err error - + for attempt := 1; attempt <= 3; attempt++ { // Increase timeout with each attempt timeout := time.Duration(attempt) * 5 * time.Second ctx, cancel := context.WithTimeout(context.Background(), timeout) - + conn, err = pool.Acquire(ctx) cancel() // Cancel the context immediately after the acquire attempt - + if err == nil { // Only log if acquisition was slow duration := time.Since(startTime) if duration > 100*time.Millisecond { - logger.Debug("Slow DB connection acquisition", + logger.Debug("Slow DB connection acquisition", zap.String("duration", duration.String()), zap.Int("attempt", attempt)) } return conn } - - logger.Warn("Failed to acquire DB connection", + + logger.Warn("Failed to acquire DB connection", zap.Int("attempt", attempt), zap.Int("maxAttempts", 3), zap.Error(err)) - + // Only log pool stats on failure logger.Warn("Pool stats after failed acquisition attempt", zap.Int32("TotalConns", pool.Stat().TotalConns()), zap.Int32("AcquiredConns", pool.Stat().AcquiredConns()), zap.Int32("IdleConns", pool.Stat().IdleConns()), zap.Int32("MaxConns", pool.Stat().MaxConns())) - + // Wait a short time before retrying to give connections a chance to be released time.Sleep(time.Duration(attempt*100) * time.Millisecond) } - + // All attempts failed logger.Error(fmt.Errorf("failed to acquire from Postgres pool after 3 attempts: %w", err)) panic("failed to acquire from Postgres pool: " + err.Error()) @@ -152,26 +152,26 @@ func monitorPoolHealth() { // Check every 30 seconds ticker := time.NewTicker(30 * time.Second) defer ticker.Stop() - + for { <-ticker.C - + if pool == nil { logger.Warn("Cannot monitor pool health: pool is nil") continue } - + stats := pool.Stat() - + // Only log if pool usage is significant if stats.AcquiredConns() > stats.MaxConns()*20/100 { - logger.Info("DB Pool Health", + logger.Info("DB Pool Health", zap.Int32("Total", stats.TotalConns()), zap.Int32("Acquired", stats.AcquiredConns()), zap.Int32("Idle", stats.IdleConns()), zap.Int32("Max", stats.MaxConns())) } - + // Check if the pool is approaching saturation if stats.AcquiredConns() > stats.MaxConns()*80/100 { logger.Warn("DB Pool nearing saturation", @@ -179,10 +179,10 @@ func monitorPoolHealth() { zap.Int32("MaxConns", stats.MaxConns()), zap.Float64("UsagePercent", float64(stats.AcquiredConns())/float64(stats.MaxConns())*100)) } - + // Test a connection to make sure the pool is working properly ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - + // Try to acquire a connection conn, err := pool.Acquire(ctx) if err != nil { @@ -190,15 +190,15 @@ func monitorPoolHealth() { cancel() continue } - + // Run a simple query to verify the connection is working var result int err = conn.QueryRow(ctx, "SELECT 1").Scan(&result) - + // Always release the connection conn.Release() cancel() - + if err != nil { logger.Error(fmt.Errorf("health check query failed: %w", err)) } else if result != 1 { @@ -206,4 +206,4 @@ func monitorPoolHealth() { } // Removed the "DB health check: connection test passed" message to reduce noise } -} \ No newline at end of file +} diff --git a/pkg/workspace/rendered.go b/pkg/workspace/rendered.go index dcdd0fd8..d1d85368 100644 --- a/pkg/workspace/rendered.go +++ b/pkg/workspace/rendered.go @@ -62,7 +62,7 @@ func FailRendered(ctx context.Context, id string, errorMessage string) error { func GetRendered(ctx context.Context, id string) (*types.Rendered, error) { startTime := time.Now() logger.Info("GetRendered", zap.String("id", id)) - + // Add panic recovery defer func() { if r := recover(); r != nil { @@ -70,7 +70,7 @@ func GetRendered(ctx context.Context, id string) (*types.Rendered, error) { zap.String("id", id)) debug.PrintStack() } - + logger.Debug("GetRendered completed", zap.String("id", id), zap.Duration("duration", time.Since(startTime))) @@ -82,35 +82,35 @@ func GetRendered(ctx context.Context, id string) (*types.Rendered, error) { logger.Debug("Got DB connection", zap.String("id", id)) query := `SELECT id, workspace_id, revision_number, created_at, completed_at, is_autorender FROM workspace_rendered WHERE id = $1` - logger.Debug("Executing first query", + logger.Debug("Executing first query", zap.String("id", id), zap.String("query", query)) - + row := conn.QueryRow(ctx, query, id) logger.Debug("Got row from first query", zap.String("id", id)) var rendered types.Rendered var completedAt sql.NullTime - + logger.Debug("About to scan row", zap.String("id", id)) if err := row.Scan(&rendered.ID, &rendered.WorkspaceID, &rendered.RevisionNumber, &rendered.CreatedAt, &completedAt, &rendered.IsAutorender); err != nil { logger.Error(fmt.Errorf("failed to scan row: %w", err), zap.String("id", id)) return nil, fmt.Errorf("failed to get rendered: %w", err) } - logger.Debug("Successfully scanned row", + logger.Debug("Successfully scanned row", zap.String("id", id), zap.String("workspaceID", rendered.WorkspaceID), zap.Int("revisionNumber", rendered.RevisionNumber)) rendered.CompletedAt = &completedAt.Time - + query = `SELECT id, chart_id, is_success, dep_update_command, dep_update_stdout, dep_update_stderr, helm_template_command, helm_template_stdout, helm_template_stderr, created_at, completed_at FROM workspace_rendered_chart WHERE workspace_render_id = $1` - - logger.Debug("Executing second query for charts", + + logger.Debug("Executing second query for charts", zap.String("id", id), zap.String("workspaceID", rendered.WorkspaceID)) - + rows, err := conn.Query(ctx, query, id) if err != nil { logger.Error(fmt.Errorf("failed to query rendered charts: %w", err), @@ -118,23 +118,23 @@ func GetRendered(ctx context.Context, id string) (*types.Rendered, error) { zap.String("workspaceID", rendered.WorkspaceID)) return nil, fmt.Errorf("failed to get rendered charts: %w", err) } - - logger.Debug("Successfully executed charts query", + + logger.Debug("Successfully executed charts query", zap.String("id", id), zap.String("workspaceID", rendered.WorkspaceID)) - + defer rows.Close() rowCount := 0 - logger.Debug("Starting to iterate through rendered chart rows", + logger.Debug("Starting to iterate through rendered chart rows", zap.String("id", id)) - + for rows.Next() { rowCount++ - logger.Debug("Processing chart row", + logger.Debug("Processing chart row", zap.String("id", id), zap.Int("rowNumber", rowCount)) - + var renderedChart types.RenderedChart var depUpdateCommand sql.NullString @@ -146,18 +146,18 @@ func GetRendered(ctx context.Context, id string) (*types.Rendered, error) { var completedAt sql.NullTime - logger.Debug("About to scan chart row", + logger.Debug("About to scan chart row", zap.String("id", id), zap.Int("rowNumber", rowCount)) - + if err := rows.Scan(&renderedChart.ID, &renderedChart.ChartID, &renderedChart.IsSuccess, &depUpdateCommand, &depUpdateStdout, &depUpdateStderr, &helmTemplateCommand, &helmTemplateStdout, &helmTemplateStderr, &renderedChart.CreatedAt, &completedAt); err != nil { logger.Error(fmt.Errorf("failed to scan chart row: %w", err), zap.String("id", id), zap.Int("rowNumber", rowCount)) return nil, fmt.Errorf("failed to get rendered chart: %w", err) } - - logger.Debug("Successfully scanned chart row", + + logger.Debug("Successfully scanned chart row", zap.String("id", id), zap.String("chartID", renderedChart.ChartID), zap.Int("rowNumber", rowCount)) @@ -171,13 +171,13 @@ func GetRendered(ctx context.Context, id string) (*types.Rendered, error) { renderedChart.CompletedAt = &completedAt.Time rendered.Charts = append(rendered.Charts, renderedChart) - logger.Debug("Added chart to rendered object", + logger.Debug("Added chart to rendered object", zap.String("id", id), zap.String("chartID", renderedChart.ChartID), zap.Int("currentChartCount", len(rendered.Charts))) } - logger.Debug("Completed processing all chart rows", + logger.Debug("Completed processing all chart rows", zap.String("id", id), zap.Int("totalRowsProcessed", rowCount), zap.Int("finalChartCount", len(rendered.Charts))) @@ -432,4 +432,4 @@ func EnqueueRenderWorkspace(ctx context.Context, workspaceID string, chatMessage } return EnqueueRenderWorkspaceForRevision(ctx, workspaceID, w.CurrentRevision, chatMessageID) -} \ No newline at end of file +} diff --git a/replicated/helmchart.yaml b/replicated/helmchart.yaml index 29595f08..b72c711d 100644 --- a/replicated/helmchart.yaml +++ b/replicated/helmchart.yaml @@ -6,9 +6,9 @@ spec: chart: name: chartsmith chartVersion: 0.3.0 - + releaseName: chartsmith - + values: # External Database Configuration postgresql: @@ -18,28 +18,28 @@ spec: username: 'repl{{ ConfigOption "embedded_database_username" }}' password: 'repl{{ ConfigOption "embedded_database_password" }}' database: 'repl{{ ConfigOption "embedded_database_name" }}' - + # LLM API Keys anthropic: apiKey: 'repl{{ ConfigOption "anthropic_api_key" }}' - + groq: apiKey: 'repl{{ ConfigOption "groq_api_key" }}' - + voyage: apiKey: 'repl{{ ConfigOption "voyage_api_key" }}' - + # Authentication Configuration auth: google: clientId: 'repl{{ ConfigOption "google_client_id" }}' clientSecret: 'repl{{ ConfigOption "google_client_secret" }}' redirectUri: 'repl{{ ConfigOption "google_redirect_uri" }}' - + # HMAC secret for JWT tokens hmac: secret: 'repl{{ ConfigOption "hmac_secret" }}' - + # Centrifugo configuration centrifugo: enabled: true @@ -47,7 +47,7 @@ spec: tokenHmacSecret: 'repl{{ ConfigOption "centrifugo_token_hmac_secret" }}' adminPassword: 'repl{{ ConfigOption "centrifugo_admin_password" }}' adminSecret: 'repl{{ ConfigOption "centrifugo_admin_secret" }}' - + builder: postgresql: enabled: true @@ -74,3 +74,679 @@ spec: tokenHmacSecret: "exampletokenhmac" adminPassword: "exampleadminpass" adminSecret: "exampleadminsecret" + docs: + reference: | + # Chartsmith Helm Chart Configuration Reference + + This document provides a comprehensive reference for all configuration options available in the Chartsmith Helm chart. + + ## Table of Contents + + - [Container Images](#container-images) + - [Service Account](#service-account) + - [Pod Configuration](#pod-configuration) + - [Service Configuration](#service-configuration) + - [Health Checks](#health-checks) + - [Resource Management](#resource-management) + - [Ingress Configuration](#ingress-configuration) + - [Public Endpoint Configuration](#public-endpoint-configuration) + - [Autoscaling](#autoscaling) + - [Metrics and Monitoring](#metrics-and-monitoring) + - [Worker Configuration](#worker-configuration) + - [Authentication](#authentication) + - [API Keys](#api-keys) + - [Database Configuration](#database-configuration) + - [Real-time Communication (Centrifugo)](#real-time-communication-centrifugo) + - [Deployment Configuration](#deployment-configuration) + + --- + + ## Container Images + + Configure container images for all Chartsmith components. + + ### `images.app` + **Required:** No + **Default:** Uses proxy.replicated.com registry + + ```yaml + images: + app: + registry: proxy.replicated.com # Container registry + repository: proxy/chartsmith/dockerhub/chartsmith/chartsmith-app # Image repository + tag: "" # Image tag (defaults to chart appVersion) + pullPolicy: IfNotPresent # Image pull policy + ``` + + ### `images.worker` + **Required:** No + **Default:** Uses proxy.replicated.com registry + + ```yaml + images: + worker: + registry: proxy.replicated.com # Container registry + repository: proxy/chartsmith/dockerhub/chartsmith/chartsmith-worker # Worker image repository + tag: "" # Image tag (defaults to chart appVersion) + pullPolicy: IfNotPresent # Image pull policy + ``` + + ### `images.centrifugo` + **Required:** No + **Default:** Uses proxy.replicated.com registry + + ```yaml + images: + centrifugo: + registry: proxy.replicated.com # Container registry + repository: proxy/chartsmith/dockerhub/centrifugo/centrifugo # Centrifugo image repository + tag: "v5" # Image tag + pullPolicy: IfNotPresent # Image pull policy + ``` + + ### `images.pgvector` + **Required:** No + **Default:** Uses proxy.replicated.com registry + + ```yaml + images: + pgvector: + registry: proxy.replicated.com # Container registry + repository: proxy/chartsmith/dockerhub/ankane/pgvector # PostgreSQL with pgvector extension + tag: "latest" # Image tag + pullPolicy: IfNotPresent # Image pull policy + ``` + + --- + + ## Service Account + + Configure Kubernetes service account for Chartsmith pods. + + ### `serviceAccount` + **Required:** No + **Default:** Creates a new service account + + ```yaml + serviceAccount: + create: true # Whether to create a service account + automount: true # Whether to automount the service account token + annotations: {} # Annotations to add to the service account + name: "" # Name of the service account (auto-generated if empty) + ``` + + --- + + ## Pod Configuration + + Configure pod-level settings for Chartsmith deployments. + + ### `nameOverride` and `fullnameOverride` + **Required:** No + **Default:** Uses chart name + + ```yaml + nameOverride: "" # Override the name of the chart + fullnameOverride: "" # Override the full name of resources + ``` + + ### `podAnnotations` + **Required:** No + **Default:** No annotations + + ```yaml + podAnnotations: {} # Kubernetes annotations to add to pods + # Example: + # podAnnotations: + # prometheus.io/scrape: "true" + # prometheus.io/port: "8080" + ``` + + ### `podLabels` + **Required:** No + **Default:** No additional labels + + ```yaml + podLabels: {} # Kubernetes labels to add to pods + # Example: + # podLabels: + # environment: production + # team: platform + ``` + + ### `podSecurityContext` + **Required:** No + **Default:** Empty (uses cluster defaults) + + ```yaml + podSecurityContext: {} # Pod-level security context + # Example: + # podSecurityContext: + # fsGroup: 2000 + # runAsNonRoot: true + ``` + + ### `securityContext` + **Required:** No + **Default:** Empty (uses cluster defaults) + + ```yaml + securityContext: {} # Container-level security context + # Example: + # securityContext: + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + ``` + + ### `nodeSelector` + **Required:** No + **Default:** No node selection + + ```yaml + nodeSelector: {} # Node selector for pod scheduling + # Example: + # nodeSelector: + # kubernetes.io/arch: amd64 + ``` + + ### `tolerations` + **Required:** No + **Default:** No tolerations + + ```yaml + tolerations: [] # Tolerations for pod scheduling + # Example: + # tolerations: + # - key: "key1" + # operator: "Equal" + # value: "value1" + # effect: "NoSchedule" + ``` + + ### `affinity` + **Required:** No + **Default:** No affinity rules + + ```yaml + affinity: {} # Affinity rules for pod scheduling + # Example: + # affinity: + # podAntiAffinity: + # preferredDuringSchedulingIgnoredDuringExecution: + # - weight: 100 + # podAffinityTerm: + # labelSelector: + # matchExpressions: + # - key: app.kubernetes.io/name + # operator: In + # values: + # - chartsmith + # topologyKey: kubernetes.io/hostname + ``` + + --- + + ## Service Configuration + + Configure Kubernetes service for external access. + + ### `service` + **Required:** No + **Default:** NodePort service on port 3000 + + ```yaml + service: + type: NodePort # Service type (ClusterIP, NodePort, LoadBalancer) + port: 3000 # Service port + targetPort: 3000 # Container port + nodePort: 30080 # NodePort (only used when type is NodePort) + annotations: {} # Service annotations + labels: {} # Service labels + ``` + + **Service Types:** + - `ClusterIP`: Internal access only + - `NodePort`: Access via node IP and port + - `LoadBalancer`: External load balancer (cloud provider dependent) + + --- + + ## Health Checks + + Configure liveness and readiness probes for application health monitoring. + + ### `livenessProbe` + **Required:** No + **Default:** HTTP probe on root path + + ```yaml + livenessProbe: + httpGet: + path: / # Health check endpoint + port: http # Port name or number + initialDelaySeconds: 30 # Delay before first probe + periodSeconds: 10 # Probe frequency + ``` + + ### `readinessProbe` + **Required:** No + **Default:** HTTP probe on root path + + ```yaml + readinessProbe: + httpGet: + path: / # Readiness check endpoint + port: http # Port name or number + initialDelaySeconds: 5 # Delay before first probe + periodSeconds: 5 # Probe frequency + ``` + + --- + + ## Resource Management + + Configure CPU and memory resources for containers. + + ### `resources` + **Required:** No + **Default:** 1 CPU / 1Gi memory limit, 100m CPU / 128Mi memory request + + ```yaml + resources: + limits: + cpu: 1000m # Maximum CPU (1 core) + memory: 1Gi # Maximum memory (1 GiB) + requests: + cpu: 100m # Requested CPU (0.1 core) + memory: 128Mi # Requested memory (128 MiB) + ``` + + **Resource Units:** + - CPU: `100m` = 0.1 core, `1000m` = 1 core + - Memory: `128Mi` = 128 MiB, `1Gi` = 1 GiB + + --- + + ## Ingress Configuration + + Configure ingress for external HTTP/HTTPS access. + + ### `ingress` + **Required:** No + **Default:** Disabled + + ```yaml + ingress: + enabled: false # Enable ingress + className: "" # Ingress class name + annotations: {} # Ingress annotations + hosts: [] # Host configurations + tls: [] # TLS configurations + ``` + + **Example Configuration:** + ```yaml + ingress: + enabled: true + className: "nginx" + annotations: + nginx.ingress.kubernetes.io/proxy-read-timeout: "3600" + nginx.ingress.kubernetes.io/proxy-send-timeout: "3600" + hosts: + - host: chartsmith.example.com + paths: + - path: /connection/websocket + pathType: Prefix + service: centrifugo + - path: / + pathType: Prefix + service: app + tls: + - secretName: chartsmith-tls + hosts: + - chartsmith.example.com + ``` + + --- + + ## Public Endpoint Configuration + + Configure public API and WebSocket endpoints for the frontend. + + ### `config` + **Required:** Yes (for frontend functionality) + **Default:** localhost development URLs + + ```yaml + config: + # Public API endpoint URL that the frontend will use + apiEndpoint: "http://chartsmith.localdev.me:3000/api" + + # WebSocket endpoint for real-time updates via Centrifugo + centrifugoAddress: "ws://chartsmith.localdev.me:3000/centrifugo/connection" + ``` + + **⚠️ Important:** These URLs must be accessible from users' browsers and should match your ingress configuration. + + --- + + ## Autoscaling + + Configure horizontal pod autoscaling based on CPU and memory usage. + + ### `autoscaling` + **Required:** No + **Default:** Disabled + + ```yaml + autoscaling: + enabled: false # Enable horizontal pod autoscaling + minReplicas: 1 # Minimum number of replicas + maxReplicas: 100 # Maximum number of replicas + targetCPUUtilizationPercentage: 80 # Target CPU utilization + targetMemoryUtilizationPercentage: 80 # Target memory utilization + ``` + + **Note:** Requires metrics-server to be installed in the cluster. + + --- + + ## Metrics and Monitoring + + Configure metrics collection and monitoring integration. + + ### `metrics` + **Required:** No + **Default:** Disabled + + ```yaml + metrics: + enabled: false # Enable metrics collection + serviceMonitor: + enabled: false # Enable Prometheus ServiceMonitor + interval: 30s # Scrape interval + ``` + + ### `configmap` + **Required:** No + **Default:** Enabled with empty data + + ```yaml + configmap: + enabled: true # Enable additional ConfigMap + data: {} # Additional configuration data + ``` + + --- + + ## Worker Configuration + + Configure the Chartsmith worker component for background processing. + + ### `worker` + **Required:** No + **Default:** Bootstrap disabled, no AWS configuration + + ```yaml + worker: + bootstrap: + enabled: false # Bootstrap initial workspace on startup + workspaceDir: "" # Custom workspace directory (default: "/bootstrap/default-workspace") + aws: + useEC2Parameters: false # Use EC2 instance metadata for AWS configuration + region: "" # AWS region (default: "us-east-1") + ``` + + **Bootstrap Options:** + - When `bootstrap.enabled` is `true`, the worker will create an initial workspace on startup + - Use `workspaceDir` to specify a custom path for bootstrap data + + **AWS Integration:** + - Set `aws.useEC2Parameters` to `true` when running on EC2 instances + - Specify `aws.region` for AWS service calls + + --- + + ## Authentication + + Configure authentication providers and security settings. + + ### `auth.enableTestAuth` + **Required:** No + **Default:** `false` + + ```yaml + auth: + enableTestAuth: false # ⚠️ NEVER USE IN PRODUCTION - enables test authentication + ``` + + **⚠️ Security Warning:** Never enable test authentication in production environments. + + ### `auth.google` (Google OAuth) + **Required:** Yes (for Google authentication) + **Default:** Empty + + ```yaml + auth: + google: + redirectUri: "" # OAuth redirect URI (must match Google OAuth configuration) + clientId: "" # Google OAuth client ID + clientSecret: "" # Google OAuth client secret (or use existingK8sSecret) + existingK8sSecret: "" # Existing Kubernetes secret (key must be GOOGLE_CLIENT_SECRET) + ``` + + **Setup Instructions:** + 1. Create OAuth credentials at https://console.cloud.google.com/ + 2. Set authorized redirect URI to match your `redirectUri` value + 3. Either provide `clientSecret` directly or reference an existing secret + + ### `auth.replicated` (Replicated OAuth) + **Required:** No + **Default:** Empty + + ```yaml + auth: + replicated: + redirectUri: "" # Replicated OAuth redirect URI (leave empty if not using) + ``` + + --- + + ## API Keys + + Configure API keys for external services. All API keys are required for full functionality. + + ### `hmac` (JWT Token Signing) + **Required:** Yes + **Default:** Empty + + ```yaml + hmac: + secret: "" # 32-byte hex string for signing JWTs + existingK8sSecret: "" # Existing Kubernetes secret (key must be HMAC_SECRET) + ``` + + **Generate HMAC Secret:** + ```bash + openssl rand -hex 32 + ``` + + ### `anthropic` (Claude AI) + **Required:** Yes + **Default:** Empty + + ```yaml + anthropic: + apiKey: "" # Anthropic API key (starts with 'sk-ant-') + existingSecret: "" # Existing Kubernetes secret (key must be ANTHROPIC_API_KEY) + ``` + + **Get API Key:** https://console.anthropic.com/ + + ### `groq` (Groq AI) + **Required:** Yes + **Default:** Empty + + ```yaml + groq: + apiKey: "" # Groq API key (starts with 'gsk_') + existingSecret: "" # Existing Kubernetes secret (key must be GROQ_API_KEY) + ``` + + **Get API Key:** https://console.groq.com/docs/quickstart + + ### `voyage` (Voyage AI Embeddings) + **Required:** Yes + **Default:** Empty + + ```yaml + voyage: + apiKey: "" # Voyage AI API key (starts with 'pa-') + existingSecret: "" # Existing Kubernetes secret (key must be VOYAGE_API_KEY) + ``` + + **Get API Key:** https://docs.voyageai.com/docs/faq#how-do-i-get-the-voyage-api-key + + --- + + ## Real-time Communication (Centrifugo) + + Configure Centrifugo for real-time WebSocket communication. + + ### `centrifugo` + **Required:** Yes + **Default:** Enabled with empty credentials + + ```yaml + centrifugo: + enabled: true # Enable Centrifugo service + apiKey: "" # Centrifugo API key + tokenHmacSecret: "" # HMAC secret for token signing + adminPassword: "" # Admin interface password + adminSecret: "" # Admin interface secret + existingSecret: "" # Existing Kubernetes secret with config.json + ``` + + **Using Existing Secret:** + If using `existingSecret`, the secret must contain a `config.json` key following the [Centrifugo v5 JSON config format](https://centrifugal.dev/docs/5/server/configuration). + + **Generate Secrets:** + ```bash + # Generate API key (32 characters) + openssl rand -base64 24 + + # Generate HMAC secret (32 characters) + openssl rand -base64 24 + + # Generate admin password + openssl rand -base64 16 + + # Generate admin secret + openssl rand -base64 24 + ``` + + --- + + ## Database Configuration + + Configure PostgreSQL database with pgvector extension. + + ### `postgresql` + **Required:** Yes (either embedded or external) + **Default:** Embedded database enabled + + ```yaml + postgresql: + enabled: true # Create embedded PostgreSQL instance + externalUri: "" # External PostgreSQL connection URI + credentials: # Credentials for embedded database + username: "" # Database username + password: "" # Database password + database: "" # Database name + existingSecret: "" # Existing secret (keys: username, password, database) + storage: + size: "10Gi" # Storage size for embedded database + className: "" # Storage class name + ``` + + **Configuration Options:** + + 1. **Embedded Database** (`enabled: true`): + - Creates a PostgreSQL StatefulSet with pgvector extension + - Requires `credentials.username`, `credentials.password`, and `credentials.database` + - Uses persistent storage with configurable size + + 2. **External Database** (`enabled: false`): + - Connects to existing PostgreSQL instance + - Requires `externalUri` in format: `postgres://user:password@host:port/dbname?options` + - External database must have pgvector extension installed + + **External URI Examples:** + ``` + postgres://chartsmith:password@db.example.com:5432/chartsmith?sslmode=require + postgres://user:pass@localhost:5432/mydb?sslmode=disable + ``` + + **Database Requirements:** + - PostgreSQL 12+ with pgvector extension + - Sufficient storage for vector embeddings and application data + - Network connectivity from Kubernetes cluster + + --- + + ## Security Best Practices + + ### Secret Management + - **Never commit secrets to version control** + - Use Kubernetes secrets or external secret management systems + - Rotate API keys and secrets regularly + - Use least privilege access for database users + + ### Network Security + - Configure ingress with TLS termination + - Restrict database access to private networks only + - Use strong authentication methods + - Enable audit logging where available + + ### Resource Security + - Set appropriate resource limits and requests + - Use security contexts to run containers as non-root + - Enable pod security policies or pod security standards + - Regular security scanning of container images + + --- + + ## Troubleshooting + + ### Common Issues + + 1. **Database Connection Failures** + - Verify database credentials and connection URI + - Check network connectivity between pods and database + - Ensure pgvector extension is installed + + 2. **Authentication Issues** + - Verify OAuth redirect URIs match exactly + - Check API key formats and validity + - Ensure HMAC secret is properly configured + + 3. **Real-time Features Not Working** + - Verify Centrifugo configuration and credentials + - Check WebSocket endpoint accessibility + - Ensure proper ingress configuration for WebSocket traffic + + 4. **Resource Issues** + - Monitor CPU and memory usage + - Adjust resource limits and requests as needed + - Check for storage space issues with embedded database + + ### Getting Help + + - Check pod logs: `kubectl logs -f deployment/chartsmith-app` + - Verify configuration: `kubectl get configmap chartsmith-configs -o yaml` + - Check secrets: `kubectl get secrets` (do not expose secret values) + - Monitor resources: `kubectl top pods` diff --git a/replicated/terraform-aws.yaml b/replicated/terraform-aws.yaml new file mode 100644 index 00000000..4ef6b3aa --- /dev/null +++ b/replicated/terraform-aws.yaml @@ -0,0 +1,54 @@ +apiVersion: kots.io/v1beta1 +kind: Terraform +metadata: + name: aws-infra +spec: + # filename references a .tgz file containing Terraform modules + filename: terraform-module-v1.0.0.tgz + + # minTerraformVersion specifies the minimum Terraform version required + minTerraformVersion: "1.5.0" + + # docs provides documentation for various user-defined steps + docs: + prerequisites: | + Before installing this Terraform configuration, ensure you have: + - AWS CLI configured with credentials + - Terraform installed (>= 1.5.0) + - Sufficient AWS permissions: + - EC2FullAccess (VPC, Security Groups) + - RDSFullAccess (PostgreSQL database) + - SecretsManagerFullAccess (API key storage) + - IAMFullAccess (Service roles) + - Sufficient AWS resource limits: + - VPCs: 1 additional (if creating new VPC) + - RDS Instances: 1 additional + - Security Groups: 5 additional + install: | + This step initializes and applies the Terraform configuration. + + 1. Choose Your Deployment Scenario: + - Minimal - Create everything from scratch (recommended for new deployments) + - Existing VPC - Use your existing VPC infrastructure + - Existing Cluster - Use your existing EKS cluster + + 2. Create Infrastructure: + ```bash + cd examples/minimal # or your chosen example + cp terraform.tfvars.example terraform.tfvars + # Edit terraform.tfvars with your values + terraform init + terraform plan + terraform apply + ``` + + 3. Basic Configuration: + ```hcl + # Environment identification + environment_name = "production" + aws_region = "us-west-2" + + # Infrastructure choices + create_vpc = true # Create new VPC + create_database = true # Create PostgreSQL database + ``` diff --git a/replicated/terraform-azure.yaml b/replicated/terraform-azure.yaml new file mode 100644 index 00000000..078dc337 --- /dev/null +++ b/replicated/terraform-azure.yaml @@ -0,0 +1,57 @@ +apiVersion: kots.io/v1beta1 +kind: Terraform +metadata: + name: azure-infra +spec: + # filename references a .tgz file containing Terraform modules + filename: terraform-module-v1.0.0.tgz + + # minTerraformVersion specifies the minimum Terraform version required + minTerraformVersion: "1.5.0" + + # docs provides documentation for various user-defined steps + docs: + prerequisites: | + Before installing this Terraform configuration, ensure you have: + - Azure CLI (az) configured with credentials + - Terraform installed (>= 1.5.0) + - Sufficient Azure permissions: + - Network Contributor (VNet, Subnets, NSG) + - Azure Kubernetes Service Contributor (AKS cluster) + - SQL DB Contributor (Azure Database for PostgreSQL) + - Key Vault Administrator (Key Vault for secrets) + - Managed Identity Operator (Service identities) + - Azure subscription with sufficient quotas: + - Virtual Networks: 1 additional (if creating new VNet) + - Azure Database for PostgreSQL instances: 1 additional + - AKS clusters: 1 additional + - Standard SKU Load Balancers: 1 additional + install: | + This step initializes and applies the Terraform configuration. + + 1. Choose Your Deployment Scenario: + - Minimal - Create everything from scratch (recommended for new deployments) + - Existing VNet - Use your existing Virtual Network infrastructure + - Existing Cluster - Use your existing AKS cluster + + 2. Create Infrastructure: + ```bash + cd examples/minimal # or your chosen example + cp terraform.tfvars.example terraform.tfvars + # Edit terraform.tfvars with your values + terraform init + terraform plan + terraform apply + ``` + + 3. Basic Configuration: + ```hcl + # Environment identification + environment_name = "production" + azure_region = "eastus" + resource_group_name = "chartsmith-rg" + + # Infrastructure choices + create_vnet = true # Create new Virtual Network + create_database = true # Create Azure Database for PostgreSQL + ``` diff --git a/replicated/terraform-gcp.yaml b/replicated/terraform-gcp.yaml new file mode 100644 index 00000000..a029a022 --- /dev/null +++ b/replicated/terraform-gcp.yaml @@ -0,0 +1,61 @@ +apiVersion: kots.io/v1beta1 +kind: Terraform +metadata: + name: gcp-infra +spec: + # filename references a .tgz file containing Terraform modules + filename: terraform-module-v1.0.0.tgz + + # minTerraformVersion specifies the minimum Terraform version required + minTerraformVersion: "1.5.0" + + # docs provides documentation for various user-defined steps + docs: + prerequisites: | + Before installing this Terraform configuration, ensure you have: + - gcloud CLI configured with credentials + - Terraform installed (>= 1.5.0) + - Sufficient GCP permissions: + - Compute Network Admin (VPC, Subnets, Firewall) + - Kubernetes Engine Admin (GKE cluster) + - Cloud SQL Admin (PostgreSQL database) + - Secret Manager Admin (API key storage) + - Service Account Admin (Service accounts and IAM) + - APIs enabled in your GCP project: + - Compute Engine API + - Kubernetes Engine API + - Cloud SQL Admin API + - Secret Manager API + - Sufficient GCP resource quotas: + - VPCs: 1 additional (if creating new VPC) + - Cloud SQL instances: 1 additional + - GKE clusters: 1 additional + install: | + This step initializes and applies the Terraform configuration. + + 1. Choose Your Deployment Scenario: + - Minimal - Create everything from scratch (recommended for new deployments) + - Existing VPC - Use your existing VPC infrastructure + - Existing Cluster - Use your existing GKE cluster + + 2. Create Infrastructure: + ```bash + cd examples/minimal # or your chosen example + cp terraform.tfvars.example terraform.tfvars + # Edit terraform.tfvars with your values + terraform init + terraform plan + terraform apply + ``` + + 3. Basic Configuration: + ```hcl + # Environment identification + environment_name = "production" + gcp_region = "us-central1" + gcp_project = "your-project-id" + + # Infrastructure choices + create_vpc = true # Create new VPC + create_database = true # Create Cloud SQL PostgreSQL database + ``` diff --git a/terraform/aws/README.md b/terraform/aws/README.md new file mode 100644 index 00000000..5aff3910 --- /dev/null +++ b/terraform/aws/README.md @@ -0,0 +1,185 @@ +# Chartsmith AWS Infrastructure + +Create and manage AWS infrastructure for Chartsmith deployment. + +## Overview + +This Terraform configuration creates AWS infrastructure for Chartsmith with: + +- **Flexible Infrastructure**: Create new or use existing VPC, EKS, and database +- **Security First**: Encrypted storage, private networking, least privilege access +- **Production Ready**: Multi-AZ support, automated backups, monitoring +- **Modular Design**: Use only the components you need + +## Quick Start + +1. **Choose Your Deployment Scenario**: + - [**Minimal**](examples/minimal/) - Create everything from scratch (recommended for new deployments) + - [**Existing VPC**](examples/existing-vpc/) - Use your existing VPC infrastructure + - [**Existing Cluster**](examples/existing-cluster/) - Use your existing EKS cluster (coming soon) + +2. **Create Infrastructure**: + ```bash + cd examples/minimal # or your chosen example + cp terraform.tfvars.example terraform.tfvars + # Edit terraform.tfvars with your values + terraform init + terraform plan + terraform apply + ``` + +## Architecture + +### Current Implementation (Phase 1) +``` +┌─────────────────────────────────────────────────────────────┐ +│ AWS Account │ +├─────────────────────────────────────────────────────────────┤ +│ VPC (Optional - can use existing) │ +│ ├── Public Subnets │ +│ ├── Private Subnets │ +│ │ └── EKS Cluster + Node Groups ✅ │ +│ └── Database Subnets │ +│ └── RDS PostgreSQL + pgvector ✅ │ +├─────────────────────────────────────────────────────────────┤ +│ Security Groups ✅ │ +│ ├── EKS Cluster SG │ +│ ├── EKS Nodes SG │ +│ └── RDS SG │ +├─────────────────────────────────────────────────────────────┤ +│ AWS Secrets Manager ✅ │ +│ └── Database Credentials (if created) │ +└─────────────────────────────────────────────────────────────┘ +└─────────────────────────────────────────────────────────────┘ +``` + +### Coming Soon (Phase 2) +- EKS Cluster with IRSA (IAM Roles for Service Accounts) + +## Requirements + +### AWS Prerequisites + +1. **AWS CLI configured** with credentials +2. **Terraform installed** (>= 1.0) +3. **Sufficient AWS permissions**: + - `EC2FullAccess` (VPC, Security Groups) + - `RDSFullAccess` (PostgreSQL database) + - `SecretsManagerFullAccess` (API key storage) + - `IAMFullAccess` (Service roles) + + +### AWS Resource Limits + +Ensure your account has sufficient limits: +- **VPCs**: 1 additional (if creating new VPC) +- **RDS Instances**: 1 additional +- **Security Groups**: 5 additional + +## Deployment Scenarios + +### 🆕 New Deployment (Recommended) +Use the [minimal example](examples/minimal/) to create everything from scratch: +- New VPC with proper networking +- EKS cluster with managed node groups +- PostgreSQL database with pgvector +- All security groups + +**Best for**: First-time deployments, isolated environments + +### 🔄 Existing VPC +Use the [existing VPC example](examples/existing-vpc/) to create infrastructure within your current network: +- Reuse your VPC, subnets, and networking +- Create EKS cluster and database +- Integrate with existing infrastructure + +**Best for**: Adding Chartsmith to existing AWS environments + +### 🏗️ Existing Cluster (Coming Soon) +Create minimal additional infrastructure: +- Use existing EKS cluster +- Use existing database +- Create only necessary supporting resources + +**Best for**: Organizations with established Kubernetes platforms + +## Configuration + +### Basic Configuration +```hcl +# Environment identification +environment_name = "production" +aws_region = "us-west-2" + +# Infrastructure choices +create_vpc = true # Create new VPC +create_database = true # Create PostgreSQL database +``` + +### Security Configuration +```hcl +# Restrict access (recommended) +allowed_cidr_blocks = [ + "203.0.113.0/24" # Your office IP range +] + +# Database security +database_instance_class = "db.t3.small" +backup_retention_period = 7 +``` + + +## Monitoring & Operations + +### What's Monitored +- **RDS Performance**: CPU, connections, storage +- **CloudWatch Logs**: Application and database logs +- **Secrets Access**: API key usage tracking + +### Backup Strategy +- **RDS Automated Backups**: 7-day retention (configurable) +- **Point-in-time Recovery**: Available for databases +- **Secrets Versioning**: Automatic secret rotation support + +### Security Features +- **Encryption at Rest**: All storage encrypted with KMS +- **Encryption in Transit**: TLS for all communications +- **Network Isolation**: Private subnets for sensitive resources +- **Least Privilege**: Minimal required permissions + +## Troubleshooting + +### Common Issues + +1. **VPC CIDR Conflicts** + ```bash + # Check existing VPCs + aws ec2 describe-vpcs --query 'Vpcs[*].[VpcId,CidrBlock]' + ``` + +2. **Database Connection Issues** + ```bash + # Test database connectivity + aws rds describe-db-instances --db-instance-identifier your-db-name + ``` + +3. **Permission Denied** + ```bash + # Verify AWS credentials + aws sts get-caller-identity + ``` + + +## Development Status + +### ✅ Completed (Phase 1) +- VPC module with flexible networking +- RDS PostgreSQL with pgvector extension +- Security groups for all components +- EKS cluster with managed node groups and IRSA support +- Customer example configurations + +### 🚧 In Progress (Phase 2) +- IAM roles and policies for service accounts + + diff --git a/terraform/aws/REQUIREMENTS.md b/terraform/aws/REQUIREMENTS.md new file mode 100644 index 00000000..78df8c23 --- /dev/null +++ b/terraform/aws/REQUIREMENTS.md @@ -0,0 +1,147 @@ +# Prerequisites for Chartsmith AWS Deployment + +## AWS Account Requirements + +### IAM Permissions + +Your AWS credentials need these permissions (attach these policies to your user/role): + +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "ec2:*", + "rds:*", + "secretsmanager:*", + "iam:*", + "logs:*", + "cloudwatch:*" + ], + "Resource": "*" + } + ] +} +``` + +**Or use these AWS managed policies:** +- `EC2FullAccess` +- `RDSFullAccess` +- `SecretsManagerFullAccess` +- `IAMFullAccess` +- `CloudWatchFullAccess` + +### Resource Limits + +Ensure your AWS account has sufficient service limits: + +| Service | Required | Check Command | +|---------|----------|---------------| +| VPCs | 1 additional | `aws ec2 describe-account-attributes --attribute-names supported-platforms` | +| RDS Instances | 1 additional | `aws rds describe-account-attributes` | +| Security Groups | 5 additional | `aws ec2 describe-account-attributes --attribute-names max-security-groups-per-vpc` | + +## Software Requirements + +### Local Development Machine + +1. **Terraform** (>= 1.0): + ```bash + # macOS + brew install terraform + + # Linux + wget https://releases.hashicorp.com/terraform/1.6.0/terraform_1.6.0_linux_amd64.zip + unzip terraform_1.6.0_linux_amd64.zip + sudo mv terraform /usr/local/bin/ + + # Verify + terraform version + ``` + +2. **AWS CLI** (>= 2.0): + ```bash + # macOS + brew install awscli + + # Linux + curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" + unzip awscliv2.zip + sudo ./aws/install + + # Verify + aws --version + ``` + +3. **Configure AWS CLI**: + ```bash + aws configure + # Enter: Access Key ID, Secret Access Key, Region, Output format + + # Test configuration + aws sts get-caller-identity + ``` + +## Network Requirements + +### For New VPC Deployment +- No specific network requirements +- Terraform will create isolated VPC + +### For Existing VPC Deployment +Your existing VPC must have: + +1. **Internet Gateway** attached +2. **NAT Gateway or NAT Instance** for private subnet internet access +3. **Route Tables** properly configured: + - Public subnets route 0.0.0.0/0 → Internet Gateway + - Private subnets route 0.0.0.0/0 → NAT Gateway +4. **Multiple Availability Zones** (minimum 2) +5. **Sufficient IP addresses** in subnets + +### Verify Existing VPC Setup +```bash +# Check VPC has Internet Gateway +aws ec2 describe-internet-gateways --filters "Name=attachment.vpc-id,Values=YOUR-VPC-ID" + +# Check NAT Gateway exists +aws ec2 describe-nat-gateways --filter "Name=vpc-id,Values=YOUR-VPC-ID" + +# Check subnets span multiple AZs +aws ec2 describe-subnets --filters "Name=vpc-id,Values=YOUR-VPC-ID" --query 'Subnets[*].[SubnetId,AvailabilityZone,CidrBlock]' +``` + +## Security Requirements + +### Network Access +- **Outbound Internet**: Required for downloading packages, API calls +- **Inbound Access**: Configure `allowed_cidr_blocks` to restrict access +- **Database Access**: Keep database in private subnets only + +### Recommended Security Practices +1. **Use least privilege IAM policies** +2. **Enable CloudTrail** for audit logging +3. **Restrict CIDR blocks** to your office/VPN ranges +4. **Enable MFA** on AWS account +5. **Use separate AWS accounts** for different environments + +## Pre-Deployment Checklist + +- [ ] AWS CLI configured and tested +- [ ] Terraform installed and working +- [ ] AWS account has required permissions +- [ ] All API keys obtained and tested +- [ ] Network requirements verified (if using existing VPC) +- [ ] Backup strategy planned + + +## Next Steps + +Once prerequisites are met: + +1. Choose your [deployment scenario](examples/) +2. Copy and customize `terraform.tfvars.example` +3. Run `terraform init && terraform plan && terraform apply` +4. Follow the post-deployment steps in the example README diff --git a/terraform/aws/examples/existing-vpc/README.md b/terraform/aws/examples/existing-vpc/README.md new file mode 100644 index 00000000..347e6ebd --- /dev/null +++ b/terraform/aws/examples/existing-vpc/README.md @@ -0,0 +1,208 @@ +# Existing VPC Infrastructure + +Create Chartsmith infrastructure within your existing AWS VPC. + +## What Gets Created + +- **PostgreSQL Database**: RDS PostgreSQL with pgvector extension in your VPC +- **Security Groups**: New security groups for Chartsmith components +- **Secrets**: AWS Secrets Manager secrets for API keys and credentials + +## What You Need to Provide + +- **Existing VPC ID**: Your VPC must have internet connectivity +- **Private Subnets**: For EKS nodes and database (minimum 2, different AZs) +- **Public Subnets**: For Kubernetes ingress/load balancers (minimum 2, different AZs) +- **Database Subnets**: Optional - if not provided, database will use private subnets + +## Prerequisites + +### VPC Requirements + +Your existing VPC must have: + +1. **Internet Gateway**: Attached to VPC for public subnet internet access +2. **NAT Gateway or NAT Instance**: For private subnet internet access +3. **Route Tables**: Properly configured for public and private subnets +4. **Multiple AZs**: Subnets spread across at least 2 availability zones + +### Subnet Requirements + +| Subnet Type | Purpose | Requirements | +|-------------|---------|--------------| +| Private | EKS worker nodes, Database (if no dedicated DB subnets) | Internet via NAT Gateway, different AZs | +| Public | Kubernetes ingress | Internet via Internet Gateway, different AZs | +| Database | Database only (optional) | No internet access required, different AZs | + +### How to Find Your Subnet IDs + +1. **AWS Console**: VPC → Subnets +2. **AWS CLI**: + ```bash + # List all subnets in your VPC + aws ec2 describe-subnets --filters "Name=vpc-id,Values=vpc-YOUR-VPC-ID" + + # Filter by subnet type + aws ec2 describe-subnets --filters "Name=vpc-id,Values=vpc-YOUR-VPC-ID" "Name=tag:Name,Values=*private*" + ``` + +## Configuration Guide + +### 1. VPC Information +```hcl +existing_vpc_id = "vpc-0123456789abcdef0" # Your VPC ID +``` + +### 2. Subnet Configuration +```hcl +# Private subnets (for EKS nodes and database) +existing_subnet_ids = [ + "subnet-0123456789abcdef0", # Private subnet AZ-a + "subnet-0123456789abcdef1", # Private subnet AZ-b + "subnet-0123456789abcdef2" # Private subnet AZ-c (optional) +] + +# Public subnets (for Kubernetes ingress) +existing_public_subnet_ids = [ + "subnet-0fedcba987654321", # Public subnet AZ-a + "subnet-0fedcba987654322" # Public subnet AZ-b +] + +# Database subnets (optional - will use private subnets if not provided) +# existing_database_subnet_ids = [ +# "subnet-0abcdef123456789", # Database subnet AZ-a +# "subnet-0abcdef123456790" # Database subnet AZ-b +# ] +``` + +### 3. Database Subnet Configuration (Optional) +```hcl +# Option 1: Use dedicated database subnets (recommended for production) +existing_database_subnet_ids = [ + "subnet-0abcdef123456789", # Database subnet AZ-a + "subnet-0abcdef123456790" # Database subnet AZ-b +] + +# Option 2: Omit this variable to use private subnets for database +# If existing_database_subnet_ids is empty or not provided, +# the database will automatically be placed in your private subnets +``` + +### 4. Security Configuration +```hcl +# Include your VPC CIDR and external access +allowed_cidr_blocks = [ + "10.0.0.0/16", # Your VPC CIDR block + "203.0.113.0/24" # Your office IP range +] +``` + +## Deployment Steps + +1. **Verify VPC Setup**: + ```bash + # Check VPC exists and has internet gateway + aws ec2 describe-vpcs --vpc-ids vpc-YOUR-VPC-ID + aws ec2 describe-internet-gateways --filters "Name=attachment.vpc-id,Values=vpc-YOUR-VPC-ID" + + # Check NAT Gateway exists + aws ec2 describe-nat-gateways --filter "Name=vpc-id,Values=vpc-YOUR-VPC-ID" + ``` + +2. **Configure terraform.tfvars**: + ```bash + cp terraform.tfvars.example terraform.tfvars + # Edit with your VPC and subnet IDs + ``` + +3. **Deploy**: + ```bash + terraform init + terraform plan + terraform apply + ``` + +## Network Architecture + +``` +Your Existing VPC +├── Internet Gateway (existing) +├── Public Subnets (existing) +│ └── Kubernetes Ingress/LoadBalancer (via Helm) +├── Private Subnets (existing) +│ ├── EKS Worker Nodes (will be created) +│ └── RDS Database (created here if no dedicated DB subnets) +├── Database Subnets (optional) +│ └── RDS Database (created here if dedicated DB subnets provided) +└── NAT Gateway (existing) +``` + +## Security Considerations + +### Security Groups Created + +1. **EKS Cluster Security Group**: Controls access to Kubernetes API +2. **EKS Nodes Security Group**: Controls worker node communication +3. **RDS Security Group**: Restricts database access to EKS nodes only + +### Network Security + +- Database is placed in private subnets (no internet access) +- Security groups follow least privilege principle +- All communication encrypted in transit + +## Troubleshooting + +### Common Issues + +1. **Subnet not found**: + - Verify subnet IDs are correct + - Check subnets exist in the specified region + +2. **Database creation fails**: + - Ensure private subnets are in different AZs + - Check subnet has sufficient IP addresses available + +3. **No internet connectivity**: + - Verify NAT Gateway exists and is properly configured + - Check route tables for private subnets + +4. **Security group conflicts**: + - Review existing security group rules + - Ensure no conflicting rules block required ports + +### Validation Commands + +```bash +# Verify VPC setup +aws ec2 describe-vpcs --vpc-ids YOUR-VPC-ID +aws ec2 describe-subnets --subnet-ids subnet-1 subnet-2 subnet-3 + +# Check route tables +aws ec2 describe-route-tables --filters "Name=vpc-id,Values=YOUR-VPC-ID" + +# Verify internet connectivity +aws ec2 describe-internet-gateways --filters "Name=attachment.vpc-id,Values=YOUR-VPC-ID" +aws ec2 describe-nat-gateways --filter "Name=vpc-id,Values=YOUR-VPC-ID" +``` + +## Migration from Other Examples + +### From Minimal Deployment +If you previously used the minimal example and want to migrate to existing VPC: + +1. Export data from current deployment +2. Update configuration to use existing VPC +3. Import existing resources where possible +4. Plan migration carefully to avoid downtime + +## Next Steps + +After successful deployment: + +1. **Verify database connectivity** from your existing infrastructure +2. **Test security group rules** allow proper communication +3. **Configure monitoring** for new resources +4. **Update DNS** if using custom domain +5. **Plan for EKS integration** when module becomes available + diff --git a/terraform/aws/examples/existing-vpc/terraform.tfvars.example b/terraform/aws/examples/existing-vpc/terraform.tfvars.example new file mode 100644 index 00000000..28c307ec --- /dev/null +++ b/terraform/aws/examples/existing-vpc/terraform.tfvars.example @@ -0,0 +1,73 @@ +# ============================================================================= +# EXISTING VPC INFRASTRUCTURE +# Create Chartsmith infrastructure in your existing AWS VPC +# Copy this file to terraform.tfvars and customize for your deployment +# ============================================================================= + +# ============================================================================= +# ENVIRONMENT IDENTIFICATION (REQUIRED) +# ============================================================================= +environment_name = "production" # Environment name for this deployment +aws_region = "us-west-2" # AWS region (must match your existing VPC) + +# ============================================================================= +# INFRASTRUCTURE CHOICES +# Use existing VPC, create database and other resources +# ============================================================================= +create_vpc = false # Use existing VPC +create_eks_cluster = true # Create new EKS cluster +create_database = true # Create new PostgreSQL database + +# ============================================================================= +# EXISTING VPC CONFIGURATION (REQUIRED) +# Get these from your AWS VPC console +# ============================================================================= +existing_vpc_id = "vpc-0123456789abcdef0" # Your existing VPC ID + +# Private subnets for EKS worker nodes and database +# Must be in different AZs and have internet access via NAT Gateway +existing_subnet_ids = [ + "subnet-0123456789abcdef0", # Private subnet in us-west-2a + "subnet-0123456789abcdef1", # Private subnet in us-west-2b + "subnet-0123456789abcdef2" # Private subnet in us-west-2c +] + +# Public subnets for Load Balancer +# Must be in different AZs and have internet access via Internet Gateway +existing_public_subnet_ids = [ + "subnet-0fedcba987654321", # Public subnet in us-west-2a + "subnet-0fedcba987654322" # Public subnet in us-west-2b +] + +# Database subnets (optional - will use private subnets if not provided) +# If you have dedicated database subnets, uncomment and specify them here. +# Otherwise, the database will be placed in your private subnets automatically. +# existing_database_subnet_ids = [ +# "subnet-0abcdef123456789", # Database subnet in us-west-2a +# "subnet-0abcdef123456790" # Database subnet in us-west-2b +# ] + + +# ============================================================================= +# OPTIONAL: NETWORK SECURITY +# ============================================================================= +allowed_cidr_blocks = [ + "10.0.0.0/16", # Your VPC CIDR (adjust to match your VPC) + "203.0.113.0/24" # Your office/VPN IP range +] + +# ============================================================================= +# OPTIONAL: DATABASE CONFIGURATION +# ============================================================================= +database_instance_class = "db.t3.small" +database_allocated_storage = 100 +backup_retention_period = 7 + +# ============================================================================= +# OPTIONAL: RESOURCE TAGGING +# ============================================================================= +tags = { + Environment = "production" + Team = "platform" + VPC = "existing" +} diff --git a/terraform/aws/examples/minimal/README.md b/terraform/aws/examples/minimal/README.md new file mode 100644 index 00000000..681f8ddf --- /dev/null +++ b/terraform/aws/examples/minimal/README.md @@ -0,0 +1,131 @@ +# Minimal AWS Infrastructure + +This example creates AWS infrastructure for Chartsmith with all new resources. + +## What Gets Created + +- **VPC**: New VPC with public, private, and database subnets across 3 AZs +- **PostgreSQL Database**: RDS PostgreSQL with pgvector extension +- **Security Groups**: Configured for EKS cluster and RDS database +- **Secrets**: AWS Secrets Manager secrets for all API keys and credentials +- **NAT Gateways**: For private subnet internet access + +## Prerequisites + +1. **AWS CLI configured** with appropriate permissions +2. **Terraform installed** (>= 1.0) +3. **API Keys obtained** from required services + +### Required API Keys + +Get these before deployment: + +| Service | Sign Up URL | API Key Format | +|---------|-------------|----------------| +| Anthropic Claude | https://console.anthropic.com/ | `sk-ant-...` | +| Groq | https://console.groq.com/ | `gsk_...` | +| Voyage AI | https://dash.voyageai.com/ | `pa-...` | +| Google OAuth | https://console.cloud.google.com/ | Client ID + Secret | + +### AWS Permissions Required + +Your AWS credentials need these permissions: +- `EC2FullAccess` (for VPC, security groups) +- `RDSFullAccess` (for PostgreSQL database) +- `SecretsManagerFullAccess` (for API key storage) +- `IAMFullAccess` (for service roles) + +## Quick Start + +1. **Copy configuration**: + ```bash + cp terraform.tfvars.example terraform.tfvars + ``` + +2. **Edit terraform.tfvars**: + - Set your `environment_name` + - Add all required API keys + - Customize other settings as needed + +3. **Deploy**: + ```bash + terraform init + terraform plan + terraform apply + ``` + +## Configuration Options + +### Basic Settings +```hcl +environment_name = "production" # Environment identifier (used for resource naming) +aws_region = "us-west-2" # AWS region +``` + +### Security Settings +```hcl +# Restrict access to your IP ranges (recommended) +allowed_cidr_blocks = [ + "203.0.113.0/24", # Your office + "198.51.100.0/24" # Your VPN +] +``` + +### Database Settings +```hcl +database_instance_class = "db.t3.small" # Start small, can upgrade +database_allocated_storage = 100 # GB, auto-scales up to 1TB +backup_retention_period = 7 # Days to keep backups +``` + + +## After Deployment + +1. **Check outputs**: + ```bash + terraform output + ``` + +2. **Verify database**: + - Check AWS RDS console + - Verify pgvector extension is available + +3. **Check secrets**: + - Verify all secrets in AWS Secrets Manager + - Test secret retrieval + +4. **Next steps**: + - Wait for EKS module implementation + - Configure DNS (if using custom domain) + - Set up monitoring and alerting + +## Troubleshooting + +### Common Issues + +1. **API key validation fails**: + - Check API key format (must start with correct prefix) + - Verify keys are active and have sufficient credits + +2. **VPC CIDR conflicts**: + - Change `vpc_cidr` if conflicts with existing networks + - Ensure subnet CIDRs don't overlap + +3. **RDS creation fails**: + - Check if you have sufficient RDS instance limits + - Verify subnet group has subnets in multiple AZs + +4. **Permission denied**: + - Verify AWS credentials have required permissions + - Check AWS CLI configuration: `aws sts get-caller-identity` + + +## Cleanup + +To destroy all resources: + +```bash +terraform destroy +``` + +**Warning**: This will delete your database and all data. Make sure you have backups if needed. diff --git a/terraform/aws/examples/minimal/terraform.tfvars.example b/terraform/aws/examples/minimal/terraform.tfvars.example new file mode 100644 index 00000000..0cd81622 --- /dev/null +++ b/terraform/aws/examples/minimal/terraform.tfvars.example @@ -0,0 +1,54 @@ +# ============================================================================= +# MINIMAL AWS INFRASTRUCTURE +# Creates AWS infrastructure needed for Chartsmith +# Copy this file to terraform.tfvars and customize for your deployment +# ============================================================================= + +# ============================================================================= +# ENVIRONMENT IDENTIFICATION (REQUIRED) +# ============================================================================= +environment_name = "production" # Environment name (e.g., "production", "staging", "dev") +aws_region = "us-west-2" # AWS region for deployment + +# ============================================================================= +# INFRASTRUCTURE CHOICES +# Create all infrastructure (recommended for new deployments) +# ============================================================================= +create_vpc = true # Create new VPC with subnets +create_eks_cluster = true # Create new EKS cluster +create_database = true # Create PostgreSQL database with pgvector + +# ============================================================================= +# OPTIONAL: NETWORK SECURITY +# Restrict access to your office/VPN IP ranges (recommended for production) +# ============================================================================= +# allowed_cidr_blocks = [ +# "203.0.113.0/24", # Your office IP range +# "198.51.100.0/24" # Your VPN IP range +# ] + +# ============================================================================= +# OPTIONAL: DATABASE SIZING +# ============================================================================= +# database_instance_class = "db.t3.small" # RDS instance size +# database_allocated_storage = 100 # Initial storage in GB +# backup_retention_period = 7 # Days to keep backups + +# ============================================================================= +# OPTIONAL: VPC CONFIGURATION +# ============================================================================= +# vpc_cidr = "10.0.0.0/16" # VPC CIDR block +# private_subnet_cidrs = [ # Private subnet CIDRs +# "10.0.1.0/24", +# "10.0.2.0/24", +# "10.0.3.0/24" +# ] + +# ============================================================================= +# OPTIONAL: RESOURCE TAGGING +# ============================================================================= +tags = { + Environment = "production" + Team = "platform" + Owner = "devops-team@acme-corp.com" +} diff --git a/terraform/aws/main.tf b/terraform/aws/main.tf new file mode 100644 index 00000000..27ed2ce7 --- /dev/null +++ b/terraform/aws/main.tf @@ -0,0 +1,231 @@ +# ============================================================================= +# CHARTSMITH AWS DEPLOYMENT +# Complete AWS infrastructure for self-hosted Chartsmith +# ============================================================================= + +locals { + name_prefix = "chartsmith-${var.environment_name}" + + common_tags = merge(var.tags, { + Application = "Chartsmith" + Environment = var.environment_name + ManagedBy = "Terraform" + CloudProvider = "AWS" + }) + + # Determine which subnets to use based on whether VPC is created or existing + vpc_id = var.create_vpc ? module.vpc[0].vpc_id : var.existing_vpc_id + private_subnet_ids = var.create_vpc ? module.vpc[0].private_subnet_ids : var.existing_subnet_ids + public_subnet_ids = var.create_vpc ? module.vpc[0].public_subnet_ids : var.existing_public_subnet_ids + + # Database subnet fallback logic: use dedicated database subnets if provided, otherwise fall back to private subnets + database_subnet_ids = var.create_vpc ? module.vpc[0].database_subnet_ids : ( + length(var.existing_database_subnet_ids) > 0 ? var.existing_database_subnet_ids : var.existing_subnet_ids + ) + + database_subnet_group_name = var.create_vpc ? module.vpc[0].database_subnet_group_name : "" +} + +# ============================================================================= +# VALIDATION CHECKS +# ============================================================================= +resource "terraform_data" "validate_existing_vpc_config" { + count = var.create_vpc ? 0 : 1 + + lifecycle { + precondition { + condition = var.existing_vpc_id != "" + error_message = "When using existing VPC (create_vpc = false), you must provide existing_vpc_id." + } + + precondition { + condition = length(var.existing_subnet_ids) >= 2 + error_message = "When using existing VPC (create_vpc = false), you must provide at least 2 private subnet IDs in different availability zones for existing_subnet_ids." + } + + precondition { + condition = length(var.existing_public_subnet_ids) >= 2 + error_message = "When using existing VPC (create_vpc = false), you must provide at least 2 public subnet IDs in different availability zones for existing_public_subnet_ids." + } + } +} + +# ============================================================================= +# VPC MODULE +# ============================================================================= +module "vpc" { + count = var.create_vpc ? 1 : 0 + source = "./modules/vpc" + + name_prefix = local.name_prefix + + vpc_cidr = var.vpc_cidr + availability_zones = var.availability_zones + private_subnet_cidrs = var.private_subnet_cidrs + public_subnet_cidrs = var.public_subnet_cidrs + database_subnet_cidrs = var.database_subnet_cidrs + + enable_nat_gateway = var.enable_nat_gateway + enable_vpn_gateway = var.enable_vpn_gateway + enable_vpc_endpoints = var.create_vpc ? true : false # Enable VPC endpoints when creating VPC + + tags = local.common_tags +} + +# ============================================================================= +# SECURITY GROUPS MODULE +# ============================================================================= +module "security_groups" { + source = "./modules/security-groups" + + name_prefix = local.name_prefix + vpc_id = local.vpc_id + + allowed_cidr_blocks = var.allowed_cidr_blocks + enable_bastion = false # Can be made configurable later + enable_vpc_endpoints = var.create_vpc ? true : false + allow_external_database_access = false # Keep database private + + tags = local.common_tags + + depends_on = [module.vpc] +} + +# ============================================================================= +# DATABASE CREDENTIALS SECRET (Infrastructure only) +# ============================================================================= +resource "aws_secretsmanager_secret" "database_credentials" { + count = var.create_database ? 1 : 0 + + name = "${local.name_prefix}-database-credentials" + description = "Database connection credentials for Chartsmith" + + tags = merge(local.common_tags, { + Name = "${local.name_prefix}-database-credentials" + Type = "secret" + Component = "database" + }) +} + +resource "aws_secretsmanager_secret_version" "database_credentials" { + count = var.create_database ? 1 : 0 + + secret_id = aws_secretsmanager_secret.database_credentials[0].id + secret_string = jsonencode({ + endpoint = module.rds[0].endpoint + port = module.rds[0].port + database = module.rds[0].database_name + username = module.rds[0].username + password = module.rds[0].password + connection_string = module.rds[0].connection_string + }) + + depends_on = [module.rds] +} + +# ============================================================================= +# RDS POSTGRESQL MODULE +# ============================================================================= +module "rds" { + count = var.create_database ? 1 : 0 + source = "./modules/rds" + + name_prefix = local.name_prefix + + # Instance configuration + instance_class = var.database_instance_class + engine_version = var.database_engine_version + allocated_storage = var.database_allocated_storage + storage_encrypted = true + + # Database configuration + database_name = var.database_name + username = var.database_username + + # Network configuration + subnet_ids = local.database_subnet_ids + security_group_ids = [module.security_groups.rds_security_group_id] + db_subnet_group_name = local.database_subnet_group_name + publicly_accessible = false # Keep database private + + # Backup configuration + backup_retention_period = var.backup_retention_period + backup_window = var.backup_window + maintenance_window = var.maintenance_window + + # Performance and monitoring + monitoring_interval = 60 + performance_insights_enabled = true + create_cloudwatch_alarms = true + + # High availability (can be enabled for production) + multi_az = false # Can be made configurable + + # Security + deletion_protection = true + skip_final_snapshot = false + + tags = local.common_tags + + depends_on = [module.vpc, module.security_groups] +} + +# ============================================================================= +# IAM ROLES MODULE (Placeholder - will implement next) +# ============================================================================= +# module "iam" { +# source = "./modules/iam" +# +# name_prefix = local.name_prefix +# +# # EKS cluster info for IRSA +# cluster_name = var.create_eks_cluster ? module.eks[0].cluster_name : var.existing_cluster_name +# oidc_issuer = var.create_eks_cluster ? module.eks[0].oidc_issuer : var.existing_oidc_issuer +# +# # Secrets Manager ARNs for access +# secrets_arns = module.secrets.secrets_arns +# +# tags = local.common_tags +# +# depends_on = [module.eks, module.secrets] +# } + +# ============================================================================= +# EKS CLUSTER MODULE +# ============================================================================= +module "eks" { + count = var.create_eks_cluster ? 1 : 0 + source = "./modules/eks" + + cluster_name = "${local.name_prefix}-eks" + cluster_version = var.cluster_version + + subnet_ids = local.private_subnet_ids + + security_group_ids = [ + module.security_groups.eks_cluster_security_group_id, + module.security_groups.eks_nodes_security_group_id + ] + + # Node groups configuration + node_groups = var.node_groups + + # Logging configuration + enabled_cluster_log_types = ["api", "audit", "authenticator", "controllerManager", "scheduler"] + log_retention_days = 14 + + # Encryption + enable_encryption = true + + # Addons + enable_vpc_cni_addon = true + enable_coredns_addon = true + enable_kube_proxy_addon = true + enable_ebs_csi_addon = true + + tags = local.common_tags + + depends_on = [module.vpc, module.security_groups] +} + + diff --git a/terraform/aws/modules/eks/main.tf b/terraform/aws/modules/eks/main.tf new file mode 100644 index 00000000..787c0808 --- /dev/null +++ b/terraform/aws/modules/eks/main.tf @@ -0,0 +1,323 @@ +# ============================================================================= +# EKS MODULE +# Creates EKS cluster with managed node groups and IRSA support +# ============================================================================= + +# Data sources +data "aws_caller_identity" "current" {} +data "aws_region" "current" {} + +# ============================================================================= +# EKS CLUSTER +# ============================================================================= +resource "aws_eks_cluster" "main" { + name = var.cluster_name + version = var.cluster_version + role_arn = aws_iam_role.cluster.arn + + vpc_config { + subnet_ids = var.subnet_ids + endpoint_private_access = var.endpoint_private_access + endpoint_public_access = var.endpoint_public_access + public_access_cidrs = var.public_access_cidrs + security_group_ids = var.security_group_ids + } + + # Enable EKS cluster logging + enabled_cluster_log_types = var.enabled_cluster_log_types + + # Encryption configuration + dynamic "encryption_config" { + for_each = var.enable_encryption ? [1] : [] + content { + provider { + key_arn = var.kms_key_arn != "" ? var.kms_key_arn : aws_kms_key.eks[0].arn + } + resources = ["secrets"] + } + } + + tags = merge(var.tags, { + Name = var.cluster_name + Type = "eks-cluster" + }) + + depends_on = [ + aws_iam_role_policy_attachment.cluster_AmazonEKSClusterPolicy, + aws_iam_role_policy_attachment.cluster_AmazonEKSVPCResourceController, + aws_cloudwatch_log_group.cluster + ] +} + +# ============================================================================= +# KMS KEY FOR EKS ENCRYPTION +# ============================================================================= +resource "aws_kms_key" "eks" { + count = var.enable_encryption && var.kms_key_arn == "" ? 1 : 0 + + description = "EKS encryption key for ${var.cluster_name}" + deletion_window_in_days = var.kms_key_deletion_window + + tags = merge(var.tags, { + Name = "${var.cluster_name}-eks-encryption-key" + Type = "kms-key" + }) +} + +resource "aws_kms_alias" "eks" { + count = var.enable_encryption && var.kms_key_arn == "" ? 1 : 0 + + name = "alias/${var.cluster_name}-eks-encryption" + target_key_id = aws_kms_key.eks[0].key_id +} + +# ============================================================================= +# CLOUDWATCH LOG GROUP FOR EKS CLUSTER LOGS +# ============================================================================= +resource "aws_cloudwatch_log_group" "cluster" { + name = "/aws/eks/${var.cluster_name}/cluster" + retention_in_days = var.log_retention_days + kms_key_id = var.enable_encryption && var.kms_key_arn == "" ? aws_kms_key.eks[0].arn : var.kms_key_arn + + tags = merge(var.tags, { + Name = "${var.cluster_name}-cluster-logs" + Type = "cloudwatch-log-group" + }) +} + +# ============================================================================= +# EKS CLUSTER IAM ROLE +# ============================================================================= +resource "aws_iam_role" "cluster" { + name = "${var.cluster_name}-cluster-role" + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = "sts:AssumeRole" + Effect = "Allow" + Principal = { + Service = "eks.amazonaws.com" + } + } + ] + }) + + tags = merge(var.tags, { + Name = "${var.cluster_name}-cluster-role" + Type = "iam-role" + }) +} + +resource "aws_iam_role_policy_attachment" "cluster_AmazonEKSClusterPolicy" { + policy_arn = "arn:aws:iam::aws:policy/AmazonEKSClusterPolicy" + role = aws_iam_role.cluster.name +} + +resource "aws_iam_role_policy_attachment" "cluster_AmazonEKSVPCResourceController" { + policy_arn = "arn:aws:iam::aws:policy/AmazonEKSVPCResourceController" + role = aws_iam_role.cluster.name +} + +# ============================================================================= +# EKS NODE GROUP IAM ROLE +# ============================================================================= +resource "aws_iam_role" "node_group" { + name = "${var.cluster_name}-node-group-role" + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = "sts:AssumeRole" + Effect = "Allow" + Principal = { + Service = "ec2.amazonaws.com" + } + } + ] + }) + + tags = merge(var.tags, { + Name = "${var.cluster_name}-node-group-role" + Type = "iam-role" + }) +} + +resource "aws_iam_role_policy_attachment" "node_group_AmazonEKSWorkerNodePolicy" { + policy_arn = "arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy" + role = aws_iam_role.node_group.name +} + +resource "aws_iam_role_policy_attachment" "node_group_AmazonEKS_CNI_Policy" { + policy_arn = "arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy" + role = aws_iam_role.node_group.name +} + +resource "aws_iam_role_policy_attachment" "node_group_AmazonEC2ContainerRegistryReadOnly" { + policy_arn = "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly" + role = aws_iam_role.node_group.name +} + +# ============================================================================= +# EKS MANAGED NODE GROUPS +# ============================================================================= +resource "aws_eks_node_group" "main" { + for_each = var.node_groups + + cluster_name = aws_eks_cluster.main.name + node_group_name = each.key + node_role_arn = aws_iam_role.node_group.arn + subnet_ids = var.subnet_ids + + # Instance configuration + instance_types = each.value.instance_types + disk_size = each.value.disk_size + ami_type = each.value.ami_type + capacity_type = each.value.capacity_type + + # Scaling configuration + scaling_config { + desired_size = each.value.desired_size + max_size = each.value.max_size + min_size = each.value.min_size + } + + # Update configuration + update_config { + max_unavailable_percentage = each.value.max_unavailable_percentage + } + + # Remote access configuration (optional) + dynamic "remote_access" { + for_each = each.value.key_name != "" ? [1] : [] + content { + ec2_ssh_key = each.value.key_name + source_security_group_ids = each.value.source_security_group_ids + } + } + + # Launch template (optional) + dynamic "launch_template" { + for_each = each.value.launch_template_id != "" ? [1] : [] + content { + id = each.value.launch_template_id + version = each.value.launch_template_version + } + } + + # Taints (optional) + dynamic "taint" { + for_each = each.value.taints + content { + key = taint.value.key + value = taint.value.value + effect = taint.value.effect + } + } + + # Labels + labels = merge(each.value.labels, { + "node-group" = each.key + }) + + tags = merge(var.tags, { + Name = "${var.cluster_name}-${each.key}-node-group" + Type = "eks-node-group" + NodeGroup = each.key + }) + + depends_on = [ + aws_iam_role_policy_attachment.node_group_AmazonEKSWorkerNodePolicy, + aws_iam_role_policy_attachment.node_group_AmazonEKS_CNI_Policy, + aws_iam_role_policy_attachment.node_group_AmazonEC2ContainerRegistryReadOnly, + ] +} + +# ============================================================================= +# EKS ADDONS +# ============================================================================= +resource "aws_eks_addon" "vpc_cni" { + count = var.enable_vpc_cni_addon ? 1 : 0 + + cluster_name = aws_eks_cluster.main.name + addon_name = "vpc-cni" + addon_version = var.vpc_cni_addon_version + resolve_conflicts = "OVERWRITE" + service_account_role_arn = var.vpc_cni_service_account_role_arn + + tags = merge(var.tags, { + Name = "${var.cluster_name}-vpc-cni-addon" + Type = "eks-addon" + }) + + depends_on = [aws_eks_node_group.main] +} + +resource "aws_eks_addon" "coredns" { + count = var.enable_coredns_addon ? 1 : 0 + + cluster_name = aws_eks_cluster.main.name + addon_name = "coredns" + addon_version = var.coredns_addon_version + resolve_conflicts = "OVERWRITE" + + tags = merge(var.tags, { + Name = "${var.cluster_name}-coredns-addon" + Type = "eks-addon" + }) + + depends_on = [aws_eks_node_group.main] +} + +resource "aws_eks_addon" "kube_proxy" { + count = var.enable_kube_proxy_addon ? 1 : 0 + + cluster_name = aws_eks_cluster.main.name + addon_name = "kube-proxy" + addon_version = var.kube_proxy_addon_version + resolve_conflicts = "OVERWRITE" + + tags = merge(var.tags, { + Name = "${var.cluster_name}-kube-proxy-addon" + Type = "eks-addon" + }) + + depends_on = [aws_eks_node_group.main] +} + +resource "aws_eks_addon" "ebs_csi_driver" { + count = var.enable_ebs_csi_addon ? 1 : 0 + + cluster_name = aws_eks_cluster.main.name + addon_name = "aws-ebs-csi-driver" + addon_version = var.ebs_csi_addon_version + resolve_conflicts = "OVERWRITE" + service_account_role_arn = var.ebs_csi_service_account_role_arn + + tags = merge(var.tags, { + Name = "${var.cluster_name}-ebs-csi-addon" + Type = "eks-addon" + }) + + depends_on = [aws_eks_node_group.main] +} + +# ============================================================================= +# OIDC IDENTITY PROVIDER (for IRSA) +# ============================================================================= +data "tls_certificate" "cluster" { + url = aws_eks_cluster.main.identity[0].oidc[0].issuer +} + +resource "aws_iam_openid_connect_provider" "cluster" { + client_id_list = ["sts.amazonaws.com"] + thumbprint_list = [data.tls_certificate.cluster.certificates[0].sha1_fingerprint] + url = aws_eks_cluster.main.identity[0].oidc[0].issuer + + tags = merge(var.tags, { + Name = "${var.cluster_name}-oidc-provider" + Type = "oidc-provider" + }) +} diff --git a/terraform/aws/modules/eks/outputs.tf b/terraform/aws/modules/eks/outputs.tf new file mode 100644 index 00000000..c319c831 --- /dev/null +++ b/terraform/aws/modules/eks/outputs.tf @@ -0,0 +1,165 @@ +# ============================================================================= +# EKS MODULE OUTPUTS +# ============================================================================= + +# ============================================================================= +# CLUSTER INFORMATION +# ============================================================================= +output "cluster_id" { + description = "EKS cluster ID" + value = aws_eks_cluster.main.id +} + +output "cluster_name" { + description = "EKS cluster name" + value = aws_eks_cluster.main.name +} + +output "cluster_arn" { + description = "EKS cluster ARN" + value = aws_eks_cluster.main.arn +} + +output "cluster_endpoint" { + description = "EKS cluster endpoint" + value = aws_eks_cluster.main.endpoint +} + +output "cluster_version" { + description = "EKS cluster Kubernetes version" + value = aws_eks_cluster.main.version +} + +output "cluster_platform_version" { + description = "EKS cluster platform version" + value = aws_eks_cluster.main.platform_version +} + +output "cluster_status" { + description = "EKS cluster status" + value = aws_eks_cluster.main.status +} + +# ============================================================================= +# CLUSTER SECURITY +# ============================================================================= +output "cluster_security_group_id" { + description = "EKS cluster security group ID" + value = aws_eks_cluster.main.vpc_config[0].cluster_security_group_id +} + +output "cluster_certificate_authority_data" { + description = "Base64 encoded certificate data required to communicate with the cluster" + value = aws_eks_cluster.main.certificate_authority[0].data +} + +# ============================================================================= +# OIDC PROVIDER (for IRSA) +# ============================================================================= +output "oidc_issuer_url" { + description = "The URL on the EKS cluster OIDC Issuer" + value = aws_eks_cluster.main.identity[0].oidc[0].issuer +} + +output "oidc_provider_arn" { + description = "ARN of the OIDC Provider for IRSA" + value = aws_iam_openid_connect_provider.cluster.arn +} + +# ============================================================================= +# NODE GROUPS INFORMATION +# ============================================================================= +output "node_groups" { + description = "Map of node group information" + value = { + for k, v in aws_eks_node_group.main : k => { + arn = v.arn + status = v.status + capacity_type = v.capacity_type + instance_types = v.instance_types + ami_type = v.ami_type + disk_size = v.disk_size + scaling_config = v.scaling_config + } + } +} + +output "node_group_arns" { + description = "List of node group ARNs" + value = [for ng in aws_eks_node_group.main : ng.arn] +} + +output "node_group_status" { + description = "Status of node groups" + value = { for k, v in aws_eks_node_group.main : k => v.status } +} + +# ============================================================================= +# IAM ROLES +# ============================================================================= +output "cluster_iam_role_arn" { + description = "IAM role ARN of the EKS cluster" + value = aws_iam_role.cluster.arn +} + +output "cluster_iam_role_name" { + description = "IAM role name of the EKS cluster" + value = aws_iam_role.cluster.name +} + +output "node_group_iam_role_arn" { + description = "IAM role ARN of the EKS node groups" + value = aws_iam_role.node_group.arn +} + +output "node_group_iam_role_name" { + description = "IAM role name of the EKS node groups" + value = aws_iam_role.node_group.name +} + +# ============================================================================= +# ENCRYPTION +# ============================================================================= +output "kms_key_arn" { + description = "ARN of the KMS key used for encryption" + value = var.enable_encryption ? (var.kms_key_arn != "" ? var.kms_key_arn : aws_kms_key.eks[0].arn) : null +} + +output "kms_key_id" { + description = "ID of the KMS key used for encryption" + value = var.enable_encryption && var.kms_key_arn == "" ? aws_kms_key.eks[0].key_id : null +} + +# ============================================================================= +# LOGGING +# ============================================================================= +output "cloudwatch_log_group_name" { + description = "Name of the CloudWatch log group for cluster logs" + value = aws_cloudwatch_log_group.cluster.name +} + +output "cloudwatch_log_group_arn" { + description = "ARN of the CloudWatch log group for cluster logs" + value = aws_cloudwatch_log_group.cluster.arn +} + +# ============================================================================= +# KUBECTL CONFIGURATION +# ============================================================================= +output "kubectl_config" { + description = "kubectl configuration command" + value = "aws eks update-kubeconfig --region ${data.aws_region.current.name} --name ${aws_eks_cluster.main.name}" +} + +# ============================================================================= +# ADDONS INFORMATION +# ============================================================================= +output "addons" { + description = "Information about enabled addons" + value = { + vpc_cni_enabled = var.enable_vpc_cni_addon + coredns_enabled = var.enable_coredns_addon + kube_proxy_enabled = var.enable_kube_proxy_addon + ebs_csi_enabled = var.enable_ebs_csi_addon + } +} diff --git a/terraform/aws/modules/eks/variables.tf b/terraform/aws/modules/eks/variables.tf new file mode 100644 index 00000000..ee36dcdb --- /dev/null +++ b/terraform/aws/modules/eks/variables.tf @@ -0,0 +1,190 @@ +# ============================================================================= +# EKS MODULE VARIABLES +# ============================================================================= + +variable "cluster_name" { + description = "Name of the EKS cluster" + type = string +} + +variable "cluster_version" { + description = "Kubernetes version for the EKS cluster" + type = string + default = "1.28" +} + +# ============================================================================= +# NETWORK CONFIGURATION +# ============================================================================= +variable "subnet_ids" { + description = "List of subnet IDs for the EKS cluster" + type = list(string) +} + +variable "security_group_ids" { + description = "List of security group IDs for the EKS cluster" + type = list(string) + default = [] +} + +variable "endpoint_private_access" { + description = "Enable private API server endpoint" + type = bool + default = true +} + +variable "endpoint_public_access" { + description = "Enable public API server endpoint" + type = bool + default = true +} + +variable "public_access_cidrs" { + description = "List of CIDR blocks that can access the public API server endpoint" + type = list(string) + default = ["0.0.0.0/0"] +} + +# ============================================================================= +# NODE GROUPS CONFIGURATION +# ============================================================================= +variable "node_groups" { + description = "Map of EKS node group configurations" + type = map(object({ + instance_types = list(string) + min_size = number + max_size = number + desired_size = number + disk_size = number + ami_type = optional(string, "AL2_x86_64") + capacity_type = optional(string, "ON_DEMAND") + max_unavailable_percentage = optional(number, 25) + key_name = optional(string, "") + source_security_group_ids = optional(list(string), []) + launch_template_id = optional(string, "") + launch_template_version = optional(string, "$Latest") + labels = optional(map(string), {}) + taints = optional(list(object({ + key = string + value = string + effect = string + })), []) + })) + default = { + main = { + instance_types = ["t3.medium"] + min_size = 2 + max_size = 10 + desired_size = 3 + disk_size = 50 + } + } +} + +# ============================================================================= +# LOGGING CONFIGURATION +# ============================================================================= +variable "enabled_cluster_log_types" { + description = "List of control plane logging types to enable" + type = list(string) + default = ["api", "audit", "authenticator", "controllerManager", "scheduler"] +} + +variable "log_retention_days" { + description = "Number of days to retain cluster logs" + type = number + default = 14 +} + +# ============================================================================= +# ENCRYPTION CONFIGURATION +# ============================================================================= +variable "enable_encryption" { + description = "Enable envelope encryption for Kubernetes secrets" + type = bool + default = true +} + +variable "kms_key_arn" { + description = "ARN of existing KMS key for encryption (leave empty to create new)" + type = string + default = "" +} + +variable "kms_key_deletion_window" { + description = "KMS key deletion window in days" + type = number + default = 7 +} + +# ============================================================================= +# ADDONS CONFIGURATION +# ============================================================================= +variable "enable_vpc_cni_addon" { + description = "Enable VPC CNI addon" + type = bool + default = true +} + +variable "vpc_cni_addon_version" { + description = "Version of VPC CNI addon (leave empty for latest)" + type = string + default = "" +} + +variable "vpc_cni_service_account_role_arn" { + description = "ARN of IAM role for VPC CNI service account" + type = string + default = "" +} + +variable "enable_coredns_addon" { + description = "Enable CoreDNS addon" + type = bool + default = true +} + +variable "coredns_addon_version" { + description = "Version of CoreDNS addon (leave empty for latest)" + type = string + default = "" +} + +variable "enable_kube_proxy_addon" { + description = "Enable kube-proxy addon" + type = bool + default = true +} + +variable "kube_proxy_addon_version" { + description = "Version of kube-proxy addon (leave empty for latest)" + type = string + default = "" +} + +variable "enable_ebs_csi_addon" { + description = "Enable EBS CSI driver addon" + type = bool + default = true +} + +variable "ebs_csi_addon_version" { + description = "Version of EBS CSI driver addon (leave empty for latest)" + type = string + default = "" +} + +variable "ebs_csi_service_account_role_arn" { + description = "ARN of IAM role for EBS CSI driver service account" + type = string + default = "" +} + +# ============================================================================= +# TAGS +# ============================================================================= +variable "tags" { + description = "Tags to apply to all resources" + type = map(string) + default = {} +} diff --git a/terraform/aws/modules/rds/main.tf b/terraform/aws/modules/rds/main.tf new file mode 100644 index 00000000..761210e2 --- /dev/null +++ b/terraform/aws/modules/rds/main.tf @@ -0,0 +1,308 @@ +# ============================================================================= +# RDS MODULE +# Creates PostgreSQL database with pgvector extension for Chartsmith +# ============================================================================= + +# Generate random password for database +resource "random_password" "database_password" { + length = 32 + special = true + # Exclude characters that might cause issues in connection strings + override_special = "!#$%&*()-_=+[]{}<>:?" +} + +# ============================================================================= +# RDS PARAMETER GROUP (for pgvector and performance tuning) +# ============================================================================= +resource "aws_db_parameter_group" "main" { + family = "postgres15" + name = "${var.name_prefix}-postgres-params" + + # Enable pgvector extension + parameter { + name = "shared_preload_libraries" + value = "vector" + } + + # Performance tuning parameters + parameter { + name = "max_connections" + value = var.max_connections + } + + parameter { + name = "shared_buffers" + value = "{DBInstanceClassMemory/4}" + } + + parameter { + name = "effective_cache_size" + value = "{DBInstanceClassMemory*3/4}" + } + + parameter { + name = "maintenance_work_mem" + value = "2048000" # 2GB in KB + } + + parameter { + name = "checkpoint_completion_target" + value = "0.9" + } + + parameter { + name = "wal_buffers" + value = "16384" # 16MB in KB + } + + parameter { + name = "default_statistics_target" + value = "100" + } + + parameter { + name = "random_page_cost" + value = "1.1" # Optimized for SSD + } + + parameter { + name = "effective_io_concurrency" + value = "200" # Optimized for SSD + } + + # Logging parameters + parameter { + name = "log_statement" + value = var.enable_query_logging ? "all" : "none" + } + + parameter { + name = "log_min_duration_statement" + value = var.slow_query_threshold_ms + } + + tags = merge(var.tags, { + Name = "${var.name_prefix}-postgres-params" + Type = "db-parameter-group" + }) +} + +# ============================================================================= +# RDS OPTION GROUP (for extensions) +# ============================================================================= +resource "aws_db_option_group" "main" { + name = "${var.name_prefix}-postgres-options" + option_group_description = "Option group for Chartsmith PostgreSQL with pgvector" + engine_name = "postgres" + major_engine_version = split(".", var.engine_version)[0] + + tags = merge(var.tags, { + Name = "${var.name_prefix}-postgres-options" + Type = "db-option-group" + }) +} + +# ============================================================================= +# RDS SUBNET GROUP +# ============================================================================= +resource "aws_db_subnet_group" "main" { + count = var.db_subnet_group_name == "" ? 1 : 0 + + name = "${var.name_prefix}-db-subnet-group" + subnet_ids = var.subnet_ids + + tags = merge(var.tags, { + Name = "${var.name_prefix}-db-subnet-group" + Type = "db-subnet-group" + }) +} + +# ============================================================================= +# RDS INSTANCE +# ============================================================================= +resource "aws_db_instance" "main" { + # Basic configuration + identifier = "${var.name_prefix}-postgres" + + # Engine configuration + engine = "postgres" + engine_version = var.engine_version + instance_class = var.instance_class + allocated_storage = var.allocated_storage + max_allocated_storage = var.max_allocated_storage + storage_type = var.storage_type + storage_encrypted = var.storage_encrypted + kms_key_id = var.kms_key_id + + # Database configuration + db_name = var.database_name + username = var.username + password = random_password.database_password.result + port = var.port + + # Parameter and option groups + parameter_group_name = aws_db_parameter_group.main.name + option_group_name = aws_db_option_group.main.name + + # Network configuration + db_subnet_group_name = var.db_subnet_group_name != "" ? var.db_subnet_group_name : aws_db_subnet_group.main[0].name + vpc_security_group_ids = var.security_group_ids + publicly_accessible = var.publicly_accessible + + # Backup configuration + backup_retention_period = var.backup_retention_period + backup_window = var.backup_window + maintenance_window = var.maintenance_window + copy_tags_to_snapshot = true + delete_automated_backups = var.delete_automated_backups + + # Monitoring and logging + monitoring_interval = var.monitoring_interval + monitoring_role_arn = var.monitoring_interval > 0 ? aws_iam_role.rds_monitoring[0].arn : null + + enabled_cloudwatch_logs_exports = var.enabled_cloudwatch_logs_exports + performance_insights_enabled = var.performance_insights_enabled + performance_insights_retention_period = var.performance_insights_enabled ? var.performance_insights_retention_period : null + + # Deletion protection + deletion_protection = var.deletion_protection + skip_final_snapshot = var.skip_final_snapshot + final_snapshot_identifier = var.skip_final_snapshot ? null : "${var.name_prefix}-postgres-final-snapshot-${formatdate("YYYY-MM-DD-hhmm", timestamp())}" + + # Multi-AZ and read replicas + multi_az = var.multi_az + + # Auto minor version upgrade + auto_minor_version_upgrade = var.auto_minor_version_upgrade + + # Apply changes immediately (use with caution in production) + apply_immediately = var.apply_immediately + + tags = merge(var.tags, { + Name = "${var.name_prefix}-postgres" + Type = "rds-instance" + Engine = "postgres" + }) + + depends_on = [ + aws_db_parameter_group.main, + aws_db_option_group.main + ] +} + +# ============================================================================= +# IAM ROLE FOR RDS ENHANCED MONITORING +# ============================================================================= +resource "aws_iam_role" "rds_monitoring" { + count = var.monitoring_interval > 0 ? 1 : 0 + + name = "${var.name_prefix}-rds-monitoring-role" + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = "sts:AssumeRole" + Effect = "Allow" + Principal = { + Service = "monitoring.rds.amazonaws.com" + } + } + ] + }) + + tags = merge(var.tags, { + Name = "${var.name_prefix}-rds-monitoring-role" + Type = "iam-role" + }) +} + +resource "aws_iam_role_policy_attachment" "rds_monitoring" { + count = var.monitoring_interval > 0 ? 1 : 0 + + role = aws_iam_role.rds_monitoring[0].name + policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonRDSEnhancedMonitoringRole" +} + +# ============================================================================= +# READ REPLICA (OPTIONAL) +# ============================================================================= +resource "aws_db_instance" "read_replica" { + count = var.create_read_replica ? 1 : 0 + + identifier = "${var.name_prefix}-postgres-replica" + + # Replica configuration + replicate_source_db = aws_db_instance.main.identifier + + # Instance configuration (can be different from primary) + instance_class = var.read_replica_instance_class != "" ? var.read_replica_instance_class : var.instance_class + + # Network configuration + publicly_accessible = var.publicly_accessible + + # Monitoring + monitoring_interval = var.monitoring_interval + monitoring_role_arn = var.monitoring_interval > 0 ? aws_iam_role.rds_monitoring[0].arn : null + + performance_insights_enabled = var.performance_insights_enabled + performance_insights_retention_period = var.performance_insights_enabled ? var.performance_insights_retention_period : null + + # Auto minor version upgrade + auto_minor_version_upgrade = var.auto_minor_version_upgrade + + tags = merge(var.tags, { + Name = "${var.name_prefix}-postgres-replica" + Type = "rds-read-replica" + Engine = "postgres" + }) +} + +# ============================================================================= +# CLOUDWATCH ALARMS (OPTIONAL) +# ============================================================================= +resource "aws_cloudwatch_metric_alarm" "database_cpu" { + count = var.create_cloudwatch_alarms ? 1 : 0 + + alarm_name = "${var.name_prefix}-rds-cpu-utilization" + comparison_operator = "GreaterThanThreshold" + evaluation_periods = "2" + metric_name = "CPUUtilization" + namespace = "AWS/RDS" + period = "300" + statistic = "Average" + threshold = "80" + alarm_description = "This metric monitors RDS CPU utilization" + + dimensions = { + DBInstanceIdentifier = aws_db_instance.main.id + } + + tags = merge(var.tags, { + Name = "${var.name_prefix}-rds-cpu-alarm" + Type = "cloudwatch-alarm" + }) +} + +resource "aws_cloudwatch_metric_alarm" "database_connections" { + count = var.create_cloudwatch_alarms ? 1 : 0 + + alarm_name = "${var.name_prefix}-rds-connection-count" + comparison_operator = "GreaterThanThreshold" + evaluation_periods = "2" + metric_name = "DatabaseConnections" + namespace = "AWS/RDS" + period = "300" + statistic = "Average" + threshold = var.max_connections * 0.8 # Alert at 80% of max connections + alarm_description = "This metric monitors RDS connection count" + + dimensions = { + DBInstanceIdentifier = aws_db_instance.main.id + } + + tags = merge(var.tags, { + Name = "${var.name_prefix}-rds-connections-alarm" + Type = "cloudwatch-alarm" + }) +} diff --git a/terraform/aws/modules/rds/outputs.tf b/terraform/aws/modules/rds/outputs.tf new file mode 100644 index 00000000..a0f86f74 --- /dev/null +++ b/terraform/aws/modules/rds/outputs.tf @@ -0,0 +1,179 @@ +# ============================================================================= +# RDS MODULE OUTPUTS +# ============================================================================= + +# ============================================================================= +# DATABASE CONNECTION INFORMATION +# ============================================================================= +output "endpoint" { + description = "RDS instance endpoint" + value = aws_db_instance.main.endpoint +} + +output "port" { + description = "RDS instance port" + value = aws_db_instance.main.port +} + +output "database_name" { + description = "Name of the database" + value = aws_db_instance.main.db_name +} + +output "username" { + description = "Database username" + value = aws_db_instance.main.username + sensitive = true +} + +output "password" { + description = "Database password" + value = random_password.database_password.result + sensitive = true +} + +# ============================================================================= +# CONNECTION STRING +# ============================================================================= +output "connection_string" { + description = "PostgreSQL connection string" + value = "postgres://${aws_db_instance.main.username}:${random_password.database_password.result}@${aws_db_instance.main.address}:${aws_db_instance.main.port}/${aws_db_instance.main.db_name}?sslmode=require" + sensitive = true +} + +output "connection_string_without_credentials" { + description = "PostgreSQL connection string without credentials" + value = "postgres://USERNAME:PASSWORD@${aws_db_instance.main.address}:${aws_db_instance.main.port}/${aws_db_instance.main.db_name}?sslmode=require" +} + +# ============================================================================= +# DATABASE INSTANCE INFORMATION +# ============================================================================= +output "instance_id" { + description = "RDS instance ID" + value = aws_db_instance.main.id +} + +output "instance_arn" { + description = "RDS instance ARN" + value = aws_db_instance.main.arn +} + +output "instance_class" { + description = "RDS instance class" + value = aws_db_instance.main.instance_class +} + +output "engine_version" { + description = "Database engine version" + value = aws_db_instance.main.engine_version +} + +output "allocated_storage" { + description = "Allocated storage in GB" + value = aws_db_instance.main.allocated_storage +} + +# ============================================================================= +# NETWORK INFORMATION +# ============================================================================= +output "hosted_zone_id" { + description = "Hosted zone ID of the DB instance" + value = aws_db_instance.main.hosted_zone_id +} + +output "vpc_security_group_ids" { + description = "VPC security group IDs" + value = aws_db_instance.main.vpc_security_group_ids +} + +output "db_subnet_group_name" { + description = "DB subnet group name" + value = aws_db_instance.main.db_subnet_group_name +} + +# ============================================================================= +# BACKUP INFORMATION +# ============================================================================= +output "backup_retention_period" { + description = "Backup retention period" + value = aws_db_instance.main.backup_retention_period +} + +output "backup_window" { + description = "Backup window" + value = aws_db_instance.main.backup_window +} + +output "maintenance_window" { + description = "Maintenance window" + value = aws_db_instance.main.maintenance_window +} + +# ============================================================================= +# READ REPLICA INFORMATION +# ============================================================================= +output "read_replica_endpoint" { + description = "Read replica endpoint" + value = var.create_read_replica ? aws_db_instance.read_replica[0].endpoint : null +} + +output "read_replica_id" { + description = "Read replica instance ID" + value = var.create_read_replica ? aws_db_instance.read_replica[0].id : null +} + +# ============================================================================= +# PARAMETER AND OPTION GROUPS +# ============================================================================= +output "parameter_group_name" { + description = "DB parameter group name" + value = aws_db_parameter_group.main.name +} + +output "option_group_name" { + description = "DB option group name" + value = aws_db_option_group.main.name +} + +# ============================================================================= +# MONITORING +# ============================================================================= +output "monitoring_role_arn" { + description = "Enhanced monitoring IAM role ARN" + value = var.monitoring_interval > 0 ? aws_iam_role.rds_monitoring[0].arn : null +} + +output "performance_insights_enabled" { + description = "Whether Performance Insights is enabled" + value = aws_db_instance.main.performance_insights_enabled +} + +# ============================================================================= +# CLOUDWATCH ALARMS +# ============================================================================= +output "cpu_alarm_arn" { + description = "CPU utilization alarm ARN" + value = var.create_cloudwatch_alarms ? aws_cloudwatch_metric_alarm.database_cpu[0].arn : null +} + +output "connections_alarm_arn" { + description = "Database connections alarm ARN" + value = var.create_cloudwatch_alarms ? aws_cloudwatch_metric_alarm.database_connections[0].arn : null +} + +# ============================================================================= +# CREDENTIALS FOR SECRETS MANAGER +# ============================================================================= +output "database_credentials" { + description = "Database credentials object for Secrets Manager" + value = { + endpoint = aws_db_instance.main.endpoint + port = aws_db_instance.main.port + database = aws_db_instance.main.db_name + username = aws_db_instance.main.username + password = random_password.database_password.result + } + sensitive = true +} + diff --git a/terraform/aws/modules/rds/variables.tf b/terraform/aws/modules/rds/variables.tf new file mode 100644 index 00000000..fe14a601 --- /dev/null +++ b/terraform/aws/modules/rds/variables.tf @@ -0,0 +1,247 @@ +# ============================================================================= +# RDS MODULE VARIABLES +# ============================================================================= + +variable "name_prefix" { + description = "Prefix for resource names" + type = string +} + +# ============================================================================= +# BASIC DATABASE CONFIGURATION +# ============================================================================= +variable "instance_class" { + description = "RDS instance class" + type = string + default = "db.t3.small" +} + +variable "engine_version" { + description = "PostgreSQL engine version" + type = string + default = "15.4" +} + +variable "allocated_storage" { + description = "Initial allocated storage in GB" + type = number + default = 100 +} + +variable "max_allocated_storage" { + description = "Maximum allocated storage for autoscaling in GB" + type = number + default = 1000 +} + +variable "storage_type" { + description = "Storage type (gp2, gp3, io1, io2)" + type = string + default = "gp3" +} + +variable "storage_encrypted" { + description = "Enable storage encryption" + type = bool + default = true +} + +variable "kms_key_id" { + description = "KMS key ID for storage encryption (leave empty for default key)" + type = string + default = "" +} + +# ============================================================================= +# DATABASE CREDENTIALS +# ============================================================================= +variable "database_name" { + description = "Name of the database to create" + type = string + default = "chartsmith" +} + +variable "username" { + description = "Username for the database" + type = string + default = "chartsmith" +} + +variable "port" { + description = "Database port" + type = number + default = 5432 +} + +# ============================================================================= +# NETWORK CONFIGURATION +# ============================================================================= +variable "subnet_ids" { + description = "List of subnet IDs for the DB subnet group" + type = list(string) +} + +variable "security_group_ids" { + description = "List of security group IDs" + type = list(string) +} + +variable "db_subnet_group_name" { + description = "Name of existing DB subnet group (leave empty to create new)" + type = string + default = "" +} + +variable "publicly_accessible" { + description = "Make the database publicly accessible" + type = bool + default = false +} + +# ============================================================================= +# BACKUP CONFIGURATION +# ============================================================================= +variable "backup_retention_period" { + description = "Number of days to retain backups" + type = number + default = 7 +} + +variable "backup_window" { + description = "Preferred backup window" + type = string + default = "03:00-04:00" +} + +variable "maintenance_window" { + description = "Preferred maintenance window" + type = string + default = "sun:04:00-sun:05:00" +} + +variable "delete_automated_backups" { + description = "Delete automated backups when the DB instance is deleted" + type = bool + default = true +} + +# ============================================================================= +# MONITORING AND LOGGING +# ============================================================================= +variable "monitoring_interval" { + description = "Enhanced monitoring interval in seconds (0, 1, 5, 10, 15, 30, 60)" + type = number + default = 60 + + validation { + condition = contains([0, 1, 5, 10, 15, 30, 60], var.monitoring_interval) + error_message = "Monitoring interval must be one of: 0, 1, 5, 10, 15, 30, 60." + } +} + +variable "enabled_cloudwatch_logs_exports" { + description = "List of log types to export to CloudWatch" + type = list(string) + default = ["postgresql", "upgrade"] +} + +variable "performance_insights_enabled" { + description = "Enable Performance Insights" + type = bool + default = true +} + +variable "performance_insights_retention_period" { + description = "Performance Insights retention period in days" + type = number + default = 7 + + validation { + condition = contains([7, 31, 62, 93, 124, 155, 186, 217, 248, 279, 310, 341, 372, 403, 434, 465, 496, 527, 558, 589, 620, 651, 682, 713, 731], var.performance_insights_retention_period) + error_message = "Performance Insights retention period must be a valid value." + } +} + +variable "create_cloudwatch_alarms" { + description = "Create CloudWatch alarms for the database" + type = bool + default = true +} + +# ============================================================================= +# PERFORMANCE TUNING +# ============================================================================= +variable "max_connections" { + description = "Maximum number of connections" + type = string + default = "100" +} + +variable "enable_query_logging" { + description = "Enable query logging (can impact performance)" + type = bool + default = false +} + +variable "slow_query_threshold_ms" { + description = "Log queries slower than this threshold (milliseconds)" + type = string + default = "1000" +} + +# ============================================================================= +# HIGH AVAILABILITY +# ============================================================================= +variable "multi_az" { + description = "Enable Multi-AZ deployment" + type = bool + default = false +} + +variable "create_read_replica" { + description = "Create a read replica" + type = bool + default = false +} + +variable "read_replica_instance_class" { + description = "Instance class for read replica (leave empty to use same as primary)" + type = string + default = "" +} + +# ============================================================================= +# MAINTENANCE AND UPDATES +# ============================================================================= +variable "auto_minor_version_upgrade" { + description = "Enable automatic minor version upgrades" + type = bool + default = true +} + +variable "apply_immediately" { + description = "Apply changes immediately (use with caution in production)" + type = bool + default = false +} + +variable "deletion_protection" { + description = "Enable deletion protection" + type = bool + default = true +} + +variable "skip_final_snapshot" { + description = "Skip final snapshot when deleting" + type = bool + default = false +} + +# ============================================================================= +# TAGS +# ============================================================================= +variable "tags" { + description = "Tags to apply to all resources" + type = map(string) + default = {} +} diff --git a/terraform/aws/modules/security-groups/main.tf b/terraform/aws/modules/security-groups/main.tf new file mode 100644 index 00000000..78942faa --- /dev/null +++ b/terraform/aws/modules/security-groups/main.tf @@ -0,0 +1,214 @@ +# ============================================================================= +# SECURITY GROUPS MODULE +# Creates security groups for EKS cluster and RDS database +# ============================================================================= + +# Data source for VPC information +data "aws_vpc" "main" { + id = var.vpc_id +} + +# ============================================================================= +# EKS CLUSTER SECURITY GROUP +# ============================================================================= +resource "aws_security_group" "eks_cluster" { + name_prefix = "${var.name_prefix}-eks-cluster" + vpc_id = var.vpc_id + description = "Security group for EKS cluster control plane" + + # Allow HTTPS traffic from worker nodes + ingress { + description = "HTTPS from worker nodes" + from_port = 443 + to_port = 443 + protocol = "tcp" + security_groups = [aws_security_group.eks_nodes.id] + } + + # Allow all outbound traffic + egress { + description = "All outbound traffic" + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + + tags = merge(var.tags, { + Name = "${var.name_prefix}-eks-cluster-sg" + Type = "security-group" + Component = "eks-cluster" + }) +} + +# ============================================================================= +# EKS WORKER NODES SECURITY GROUP +# ============================================================================= +resource "aws_security_group" "eks_nodes" { + name_prefix = "${var.name_prefix}-eks-nodes" + vpc_id = var.vpc_id + description = "Security group for EKS worker nodes" + + # Allow nodes to communicate with each other + ingress { + description = "Node to node communication" + from_port = 0 + to_port = 65535 + protocol = "tcp" + self = true + } + + # Allow worker Kubelets and pods to receive communication from the cluster control plane + ingress { + description = "Control plane to worker nodes" + from_port = 1025 + to_port = 65535 + protocol = "tcp" + security_groups = [aws_security_group.eks_cluster.id] + } + + # Allow pods running extension API servers on port 443 to receive communication from cluster control plane + ingress { + description = "Control plane to worker nodes (HTTPS)" + from_port = 443 + to_port = 443 + protocol = "tcp" + security_groups = [aws_security_group.eks_cluster.id] + } + + # Allow all outbound traffic + egress { + description = "All outbound traffic" + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + + tags = merge(var.tags, { + Name = "${var.name_prefix}-eks-nodes-sg" + Type = "security-group" + Component = "eks-nodes" + }) +} + +# ============================================================================= +# RDS DATABASE SECURITY GROUP +# ============================================================================= +resource "aws_security_group" "rds" { + name_prefix = "${var.name_prefix}-rds" + vpc_id = var.vpc_id + description = "Security group for RDS PostgreSQL database" + + # Allow PostgreSQL traffic from EKS nodes + ingress { + description = "PostgreSQL from EKS nodes" + from_port = 5432 + to_port = 5432 + protocol = "tcp" + security_groups = [aws_security_group.eks_nodes.id] + } + + # Allow PostgreSQL traffic from bastion host (if enabled) + dynamic "ingress" { + for_each = var.enable_bastion ? [1] : [] + content { + description = "PostgreSQL from bastion host" + from_port = 5432 + to_port = 5432 + protocol = "tcp" + security_groups = [aws_security_group.bastion[0].id] + } + } + + # Allow PostgreSQL traffic from allowed CIDR blocks (for external access) + dynamic "ingress" { + for_each = var.allow_external_database_access ? [1] : [] + content { + description = "PostgreSQL from allowed CIDR blocks" + from_port = 5432 + to_port = 5432 + protocol = "tcp" + cidr_blocks = var.allowed_cidr_blocks + } + } + + # No outbound rules needed for RDS + + tags = merge(var.tags, { + Name = "${var.name_prefix}-rds-sg" + Type = "security-group" + Component = "rds" + }) +} + + +# ============================================================================= +# BASTION HOST SECURITY GROUP (OPTIONAL) +# ============================================================================= +resource "aws_security_group" "bastion" { + count = var.enable_bastion ? 1 : 0 + + name_prefix = "${var.name_prefix}-bastion" + vpc_id = var.vpc_id + description = "Security group for bastion host" + + # Allow SSH from allowed CIDR blocks + ingress { + description = "SSH from allowed sources" + from_port = 22 + to_port = 22 + protocol = "tcp" + cidr_blocks = var.allowed_cidr_blocks + } + + # Allow all outbound traffic + egress { + description = "All outbound traffic" + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + + tags = merge(var.tags, { + Name = "${var.name_prefix}-bastion-sg" + Type = "security-group" + Component = "bastion" + }) +} + +# ============================================================================= +# VPC ENDPOINTS SECURITY GROUP (FOR PRIVATE CONNECTIVITY) +# ============================================================================= +resource "aws_security_group" "vpc_endpoints" { + count = var.enable_vpc_endpoints ? 1 : 0 + + name_prefix = "${var.name_prefix}-vpc-endpoints" + vpc_id = var.vpc_id + description = "Security group for VPC endpoints" + + # Allow HTTPS from VPC CIDR + ingress { + description = "HTTPS from VPC" + from_port = 443 + to_port = 443 + protocol = "tcp" + cidr_blocks = [data.aws_vpc.main.cidr_block] + } + + # Allow all outbound traffic + egress { + description = "All outbound traffic" + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + + tags = merge(var.tags, { + Name = "${var.name_prefix}-vpc-endpoints-sg" + Type = "security-group" + Component = "vpc-endpoints" + }) +} diff --git a/terraform/aws/modules/security-groups/outputs.tf b/terraform/aws/modules/security-groups/outputs.tf new file mode 100644 index 00000000..bef3511a --- /dev/null +++ b/terraform/aws/modules/security-groups/outputs.tf @@ -0,0 +1,49 @@ +# ============================================================================= +# SECURITY GROUPS MODULE OUTPUTS +# ============================================================================= + +output "eks_cluster_security_group_id" { + description = "ID of the EKS cluster security group" + value = aws_security_group.eks_cluster.id +} + +output "eks_nodes_security_group_id" { + description = "ID of the EKS worker nodes security group" + value = aws_security_group.eks_nodes.id +} + +output "rds_security_group_id" { + description = "ID of the RDS security group" + value = aws_security_group.rds.id +} + + +output "bastion_security_group_id" { + description = "ID of the bastion host security group" + value = var.enable_bastion ? aws_security_group.bastion[0].id : null +} + +output "vpc_endpoints_security_group_id" { + description = "ID of the VPC endpoints security group" + value = var.enable_vpc_endpoints ? aws_security_group.vpc_endpoints[0].id : null +} + +# ============================================================================= +# SECURITY GROUP ARNS (for reference) +# ============================================================================= + +output "eks_cluster_security_group_arn" { + description = "ARN of the EKS cluster security group" + value = aws_security_group.eks_cluster.arn +} + +output "eks_nodes_security_group_arn" { + description = "ARN of the EKS worker nodes security group" + value = aws_security_group.eks_nodes.arn +} + +output "rds_security_group_arn" { + description = "ARN of the RDS security group" + value = aws_security_group.rds.arn +} + diff --git a/terraform/aws/modules/security-groups/variables.tf b/terraform/aws/modules/security-groups/variables.tf new file mode 100644 index 00000000..bafecb79 --- /dev/null +++ b/terraform/aws/modules/security-groups/variables.tf @@ -0,0 +1,43 @@ +# ============================================================================= +# SECURITY GROUPS MODULE VARIABLES +# ============================================================================= + +variable "name_prefix" { + description = "Prefix for resource names" + type = string +} + +variable "vpc_id" { + description = "ID of the VPC" + type = string +} + +variable "allowed_cidr_blocks" { + description = "CIDR blocks allowed to access resources" + type = list(string) + default = ["0.0.0.0/0"] +} + +variable "enable_bastion" { + description = "Enable bastion host security group" + type = bool + default = false +} + +variable "enable_vpc_endpoints" { + description = "Enable VPC endpoints security group" + type = bool + default = false +} + +variable "allow_external_database_access" { + description = "Allow external access to database (not recommended for production)" + type = bool + default = false +} + +variable "tags" { + description = "Tags to apply to all resources" + type = map(string) + default = {} +} diff --git a/terraform/aws/modules/vpc/main.tf b/terraform/aws/modules/vpc/main.tf new file mode 100644 index 00000000..4b1e31f7 --- /dev/null +++ b/terraform/aws/modules/vpc/main.tf @@ -0,0 +1,335 @@ +# ============================================================================= +# VPC MODULE +# Creates VPC with public, private, and database subnets +# Supports flexible AZ selection and optional NAT/VPN gateways +# ============================================================================= + +locals { + # Use provided AZs or select first 3 available + availability_zones = length(var.availability_zones) > 0 ? var.availability_zones : slice(data.aws_availability_zones.available.names, 0, 3) + + # Calculate number of subnets needed + subnet_count = length(local.availability_zones) + + # Ensure we have enough CIDR blocks + private_cidrs = slice(var.private_subnet_cidrs, 0, local.subnet_count) + public_cidrs = slice(var.public_subnet_cidrs, 0, local.subnet_count) + database_cidrs = slice(var.database_subnet_cidrs, 0, local.subnet_count) +} + +# Data source for availability zones +data "aws_availability_zones" "available" { + state = "available" +} + +# ============================================================================= +# VPC +# ============================================================================= +resource "aws_vpc" "main" { + cidr_block = var.vpc_cidr + enable_dns_hostnames = true + enable_dns_support = true + + tags = merge(var.tags, { + Name = "${var.name_prefix}-vpc" + Type = "vpc" + }) +} + +# ============================================================================= +# INTERNET GATEWAY +# ============================================================================= +resource "aws_internet_gateway" "main" { + vpc_id = aws_vpc.main.id + + tags = merge(var.tags, { + Name = "${var.name_prefix}-igw" + Type = "internet-gateway" + }) +} + +# ============================================================================= +# PUBLIC SUBNETS +# ============================================================================= +resource "aws_subnet" "public" { + count = local.subnet_count + + vpc_id = aws_vpc.main.id + cidr_block = local.public_cidrs[count.index] + availability_zone = local.availability_zones[count.index] + map_public_ip_on_launch = true + + tags = merge(var.tags, { + Name = "${var.name_prefix}-public-${local.availability_zones[count.index]}" + Type = "public-subnet" + "kubernetes.io/role/elb" = "1" # For AWS Load Balancer Controller + }) +} + +# ============================================================================= +# PRIVATE SUBNETS +# ============================================================================= +resource "aws_subnet" "private" { + count = local.subnet_count + + vpc_id = aws_vpc.main.id + cidr_block = local.private_cidrs[count.index] + availability_zone = local.availability_zones[count.index] + + tags = merge(var.tags, { + Name = "${var.name_prefix}-private-${local.availability_zones[count.index]}" + Type = "private-subnet" + "kubernetes.io/role/internal-elb" = "1" # For AWS Load Balancer Controller + }) +} + +# ============================================================================= +# DATABASE SUBNETS +# ============================================================================= +resource "aws_subnet" "database" { + count = local.subnet_count + + vpc_id = aws_vpc.main.id + cidr_block = local.database_cidrs[count.index] + availability_zone = local.availability_zones[count.index] + + tags = merge(var.tags, { + Name = "${var.name_prefix}-database-${local.availability_zones[count.index]}" + Type = "database-subnet" + }) +} + +# ============================================================================= +# DATABASE SUBNET GROUP +# ============================================================================= +resource "aws_db_subnet_group" "main" { + name = "${var.name_prefix}-db-subnet-group" + subnet_ids = aws_subnet.database[*].id + + tags = merge(var.tags, { + Name = "${var.name_prefix}-db-subnet-group" + Type = "database-subnet-group" + }) +} + +# ============================================================================= +# ELASTIC IPS FOR NAT GATEWAYS +# ============================================================================= +resource "aws_eip" "nat" { + count = var.enable_nat_gateway ? local.subnet_count : 0 + + domain = "vpc" + + tags = merge(var.tags, { + Name = "${var.name_prefix}-nat-eip-${local.availability_zones[count.index]}" + Type = "elastic-ip" + }) + + depends_on = [aws_internet_gateway.main] +} + +# ============================================================================= +# NAT GATEWAYS +# ============================================================================= +resource "aws_nat_gateway" "main" { + count = var.enable_nat_gateway ? local.subnet_count : 0 + + allocation_id = aws_eip.nat[count.index].id + subnet_id = aws_subnet.public[count.index].id + + tags = merge(var.tags, { + Name = "${var.name_prefix}-nat-${local.availability_zones[count.index]}" + Type = "nat-gateway" + }) + + depends_on = [aws_internet_gateway.main] +} + +# ============================================================================= +# VPN GATEWAY (OPTIONAL) +# ============================================================================= +resource "aws_vpn_gateway" "main" { + count = var.enable_vpn_gateway ? 1 : 0 + + vpc_id = aws_vpc.main.id + + tags = merge(var.tags, { + Name = "${var.name_prefix}-vpn-gateway" + Type = "vpn-gateway" + }) +} + +# ============================================================================= +# ROUTE TABLES +# ============================================================================= + +# Public route table +resource "aws_route_table" "public" { + vpc_id = aws_vpc.main.id + + route { + cidr_block = "0.0.0.0/0" + gateway_id = aws_internet_gateway.main.id + } + + tags = merge(var.tags, { + Name = "${var.name_prefix}-public-rt" + Type = "route-table" + }) +} + +# Private route tables (one per AZ for NAT Gateway) +resource "aws_route_table" "private" { + count = local.subnet_count + + vpc_id = aws_vpc.main.id + + dynamic "route" { + for_each = var.enable_nat_gateway ? [1] : [] + content { + cidr_block = "0.0.0.0/0" + nat_gateway_id = aws_nat_gateway.main[count.index].id + } + } + + dynamic "route" { + for_each = var.enable_vpn_gateway ? [1] : [] + content { + cidr_block = var.vpn_gateway_cidr + gateway_id = aws_vpn_gateway.main[0].id + } + } + + tags = merge(var.tags, { + Name = "${var.name_prefix}-private-rt-${local.availability_zones[count.index]}" + Type = "route-table" + }) +} + +# Database route table +resource "aws_route_table" "database" { + vpc_id = aws_vpc.main.id + + dynamic "route" { + for_each = var.enable_vpn_gateway ? [1] : [] + content { + cidr_block = var.vpn_gateway_cidr + gateway_id = aws_vpn_gateway.main[0].id + } + } + + tags = merge(var.tags, { + Name = "${var.name_prefix}-database-rt" + Type = "route-table" + }) +} + +# ============================================================================= +# ROUTE TABLE ASSOCIATIONS +# ============================================================================= + +# Public subnet associations +resource "aws_route_table_association" "public" { + count = local.subnet_count + + subnet_id = aws_subnet.public[count.index].id + route_table_id = aws_route_table.public.id +} + +# Private subnet associations +resource "aws_route_table_association" "private" { + count = local.subnet_count + + subnet_id = aws_subnet.private[count.index].id + route_table_id = aws_route_table.private[count.index].id +} + +# Database subnet associations +resource "aws_route_table_association" "database" { + count = local.subnet_count + + subnet_id = aws_subnet.database[count.index].id + route_table_id = aws_route_table.database.id +} + +# ============================================================================= +# VPC ENDPOINTS (Optional - for private connectivity to AWS services) +# ============================================================================= + +# S3 VPC Endpoint +resource "aws_vpc_endpoint" "s3" { + count = var.enable_vpc_endpoints ? 1 : 0 + + vpc_id = aws_vpc.main.id + service_name = "com.amazonaws.${data.aws_region.current.name}.s3" + vpc_endpoint_type = "Gateway" + route_table_ids = concat([aws_route_table.public.id], aws_route_table.private[*].id) + + tags = merge(var.tags, { + Name = "${var.name_prefix}-s3-endpoint" + Type = "vpc-endpoint" + }) +} + +# ECR VPC Endpoints (for EKS) +resource "aws_vpc_endpoint" "ecr_dkr" { + count = var.enable_vpc_endpoints ? 1 : 0 + + vpc_id = aws_vpc.main.id + service_name = "com.amazonaws.${data.aws_region.current.name}.ecr.dkr" + vpc_endpoint_type = "Interface" + subnet_ids = aws_subnet.private[*].id + security_group_ids = [aws_security_group.vpc_endpoints[0].id] + private_dns_enabled = true + + tags = merge(var.tags, { + Name = "${var.name_prefix}-ecr-dkr-endpoint" + Type = "vpc-endpoint" + }) +} + +resource "aws_vpc_endpoint" "ecr_api" { + count = var.enable_vpc_endpoints ? 1 : 0 + + vpc_id = aws_vpc.main.id + service_name = "com.amazonaws.${data.aws_region.current.name}.ecr.api" + vpc_endpoint_type = "Interface" + subnet_ids = aws_subnet.private[*].id + security_group_ids = [aws_security_group.vpc_endpoints[0].id] + private_dns_enabled = true + + tags = merge(var.tags, { + Name = "${var.name_prefix}-ecr-api-endpoint" + Type = "vpc-endpoint" + }) +} + +# Security group for VPC endpoints +resource "aws_security_group" "vpc_endpoints" { + count = var.enable_vpc_endpoints ? 1 : 0 + + name_prefix = "${var.name_prefix}-vpc-endpoints" + vpc_id = aws_vpc.main.id + + ingress { + from_port = 443 + to_port = 443 + protocol = "tcp" + cidr_blocks = [aws_vpc.main.cidr_block] + } + + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + + tags = merge(var.tags, { + Name = "${var.name_prefix}-vpc-endpoints-sg" + Type = "security-group" + }) +} + +# Data source for current region +data "aws_region" "current" {} diff --git a/terraform/aws/modules/vpc/outputs.tf b/terraform/aws/modules/vpc/outputs.tf new file mode 100644 index 00000000..d538d252 --- /dev/null +++ b/terraform/aws/modules/vpc/outputs.tf @@ -0,0 +1,112 @@ +# ============================================================================= +# VPC MODULE OUTPUTS +# ============================================================================= + +output "vpc_id" { + description = "ID of the VPC" + value = aws_vpc.main.id +} + +output "vpc_cidr_block" { + description = "CIDR block of the VPC" + value = aws_vpc.main.cidr_block +} + +output "internet_gateway_id" { + description = "ID of the Internet Gateway" + value = aws_internet_gateway.main.id +} + +# ============================================================================= +# SUBNET OUTPUTS +# ============================================================================= + +output "public_subnet_ids" { + description = "IDs of the public subnets" + value = aws_subnet.public[*].id +} + +output "private_subnet_ids" { + description = "IDs of the private subnets" + value = aws_subnet.private[*].id +} + +output "database_subnet_ids" { + description = "IDs of the database subnets" + value = aws_subnet.database[*].id +} + +output "database_subnet_group_name" { + description = "Name of the database subnet group" + value = aws_db_subnet_group.main.name +} + +# ============================================================================= +# AVAILABILITY ZONE OUTPUTS +# ============================================================================= + +output "availability_zones" { + description = "List of availability zones used" + value = local.availability_zones +} + +# ============================================================================= +# NAT GATEWAY OUTPUTS +# ============================================================================= + +output "nat_gateway_ids" { + description = "IDs of the NAT Gateways" + value = aws_nat_gateway.main[*].id +} + +output "nat_gateway_public_ips" { + description = "Public IPs of the NAT Gateways" + value = aws_eip.nat[*].public_ip +} + +# ============================================================================= +# ROUTE TABLE OUTPUTS +# ============================================================================= + +output "public_route_table_id" { + description = "ID of the public route table" + value = aws_route_table.public.id +} + +output "private_route_table_ids" { + description = "IDs of the private route tables" + value = aws_route_table.private[*].id +} + +output "database_route_table_id" { + description = "ID of the database route table" + value = aws_route_table.database.id +} + +# ============================================================================= +# VPN GATEWAY OUTPUTS +# ============================================================================= + +output "vpn_gateway_id" { + description = "ID of the VPN Gateway" + value = var.enable_vpn_gateway ? aws_vpn_gateway.main[0].id : null +} + +# ============================================================================= +# VPC ENDPOINT OUTPUTS +# ============================================================================= + +output "vpc_endpoint_s3_id" { + description = "ID of the S3 VPC endpoint" + value = var.enable_vpc_endpoints ? aws_vpc_endpoint.s3[0].id : null +} + +output "vpc_endpoint_ecr_dkr_id" { + description = "ID of the ECR DKR VPC endpoint" + value = var.enable_vpc_endpoints ? aws_vpc_endpoint.ecr_dkr[0].id : null +} + +output "vpc_endpoint_ecr_api_id" { + description = "ID of the ECR API VPC endpoint" + value = var.enable_vpc_endpoints ? aws_vpc_endpoint.ecr_api[0].id : null +} diff --git a/terraform/aws/modules/vpc/variables.tf b/terraform/aws/modules/vpc/variables.tf new file mode 100644 index 00000000..e5125e6d --- /dev/null +++ b/terraform/aws/modules/vpc/variables.tf @@ -0,0 +1,68 @@ +# ============================================================================= +# VPC MODULE VARIABLES +# ============================================================================= + +variable "name_prefix" { + description = "Prefix for resource names" + type = string +} + +variable "vpc_cidr" { + description = "CIDR block for VPC" + type = string + default = "10.0.0.0/16" +} + +variable "availability_zones" { + description = "List of availability zones to use (leave empty for automatic selection)" + type = list(string) + default = [] +} + +variable "private_subnet_cidrs" { + description = "CIDR blocks for private subnets" + type = list(string) + default = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"] +} + +variable "public_subnet_cidrs" { + description = "CIDR blocks for public subnets" + type = list(string) + default = ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"] +} + +variable "database_subnet_cidrs" { + description = "CIDR blocks for database subnets" + type = list(string) + default = ["10.0.201.0/24", "10.0.202.0/24", "10.0.203.0/24"] +} + +variable "enable_nat_gateway" { + description = "Enable NAT Gateway for private subnets" + type = bool + default = true +} + +variable "enable_vpn_gateway" { + description = "Enable VPN Gateway" + type = bool + default = false +} + +variable "vpn_gateway_cidr" { + description = "CIDR block for VPN Gateway routes" + type = string + default = "192.168.0.0/16" +} + +variable "enable_vpc_endpoints" { + description = "Enable VPC endpoints for AWS services (S3, ECR)" + type = bool + default = false +} + +variable "tags" { + description = "Tags to apply to all resources" + type = map(string) + default = {} +} diff --git a/terraform/aws/outputs.tf b/terraform/aws/outputs.tf new file mode 100644 index 00000000..d2b7b5d9 --- /dev/null +++ b/terraform/aws/outputs.tf @@ -0,0 +1,163 @@ +# ============================================================================= +# AWS DEPLOYMENT OUTPUTS +# Information customers get after successful deployment +# ============================================================================= + +# ============================================================================= +# DEPLOYMENT INFORMATION +# ============================================================================= +output "deployment_info" { + description = "Summary of deployment information" + value = { + environment_name = var.environment_name + aws_region = var.aws_region + vpc_created = var.create_vpc + eks_created = var.create_eks_cluster + database_created = var.create_database + cluster_name = var.create_eks_cluster ? module.eks[0].cluster_name : var.existing_cluster_name + } +} + +# ============================================================================= +# NETWORK INFORMATION +# ============================================================================= +output "vpc_info" { + description = "VPC information" + value = var.create_vpc ? { + vpc_id = module.vpc[0].vpc_id + vpc_cidr = module.vpc[0].vpc_cidr_block + public_subnet_ids = module.vpc[0].public_subnet_ids + private_subnet_ids = module.vpc[0].private_subnet_ids + database_subnet_ids = module.vpc[0].database_subnet_ids + availability_zones = module.vpc[0].availability_zones + nat_gateway_ips = module.vpc[0].nat_gateway_public_ips + } : { + vpc_id = var.existing_vpc_id + message = "Using existing VPC" + } +} + +# ============================================================================= +# DATABASE INFORMATION +# ============================================================================= +output "database_info" { + description = "Database connection information" + value = var.create_database ? { + endpoint = module.rds[0].endpoint + port = module.rds[0].port + database_name = module.rds[0].database_name + instance_class = module.rds[0].instance_class + engine_version = module.rds[0].engine_version + backup_retention = module.rds[0].backup_retention_period + multi_az = false # Will be configurable later + encrypted = true + } : { + endpoint = var.existing_database_endpoint + message = "Using existing database" + } + sensitive = true +} + +# ============================================================================= +# EKS CLUSTER INFORMATION +# ============================================================================= +output "eks_info" { + description = "EKS cluster information" + value = var.create_eks_cluster ? { + cluster_name = module.eks[0].cluster_name + cluster_endpoint = module.eks[0].cluster_endpoint + cluster_version = module.eks[0].cluster_version + cluster_status = module.eks[0].cluster_status + oidc_issuer_url = module.eks[0].oidc_issuer_url + node_groups = keys(var.node_groups) + kubectl_config = module.eks[0].kubectl_config + } : { + cluster_name = var.existing_cluster_name + message = "Using existing EKS cluster" + } +} + +# ============================================================================= +# SECRETS INFORMATION +# ============================================================================= +output "secrets_info" { + description = "Information about created secrets" + value = { + secrets_created = var.create_database ? ["database-credentials"] : [] + secrets_region = var.aws_region + } +} + +# ============================================================================= +# SECURITY GROUPS +# ============================================================================= +output "security_groups" { + description = "Security group information" + value = { + eks_cluster_sg = module.security_groups.eks_cluster_security_group_id + eks_nodes_sg = module.security_groups.eks_nodes_security_group_id + rds_sg = module.security_groups.rds_security_group_id + } +} + +# ============================================================================= +# NEXT STEPS FOR CUSTOMER +# ============================================================================= +output "next_steps" { + description = "What to do after deployment" + value = <<-EOT + ✅ Chartsmith AWS infrastructure deployed successfully! + + 📋 What was created: + ${var.create_vpc ? "- VPC with public, private, and database subnets" : "- Used existing VPC"} + ${var.create_eks_cluster ? "- EKS cluster with managed node groups" : "- Configured for existing EKS cluster"} + ${var.create_database ? "- PostgreSQL database with pgvector extension" : "- Configured for existing database"} + - Security groups for EKS cluster and RDS database + ${var.create_database ? "- Database credentials in AWS Secrets Manager" : ""} + + 🚧 Still needed (coming in next modules): + - IAM roles for service accounts (IRSA) + + 📚 Resources: + - VPC ID: ${local.vpc_id} + ${var.create_eks_cluster ? "- EKS Cluster: ${module.eks[0].cluster_name}" : "- EKS Cluster: ${var.existing_cluster_name} (existing)"} + - Database: ${var.create_database ? module.rds[0].endpoint : var.existing_database_endpoint} + ${var.create_database ? "- Database credentials: AWS Secrets Manager in ${var.aws_region}" : ""} + + 🔧 Manual steps required: + 1. Configure kubectl access: ${var.create_eks_cluster ? module.eks[0].kubectl_config : "aws eks update-kubeconfig --name ${var.existing_cluster_name}"} + 2. Deploy Chartsmith via Helm + 3. Verify database connectivity and pgvector extension + + EOT +} + +# ============================================================================= +# TERRAFORM STATE INFORMATION +# ============================================================================= +output "terraform_info" { + description = "Terraform state information" + value = { + terraform_version = "~> 1.0" + aws_provider_version = "~> 5.0" + region = var.aws_region + state_backend = "Configure remote state backend for production use" + } +} + + +# ============================================================================= +# SENSITIVE OUTPUTS (for automation) +# ============================================================================= +output "database_connection_string" { + description = "Database connection string" + value = var.create_database ? module.rds[0].connection_string : "Configure manually for existing database" + sensitive = true +} + +output "database_secret_arn" { + description = "ARN of database credentials secret" + value = var.create_database ? aws_secretsmanager_secret.database_credentials[0].arn : null + sensitive = true +} + diff --git a/terraform/aws/terraform.tf b/terraform/aws/terraform.tf new file mode 100644 index 00000000..5da98432 --- /dev/null +++ b/terraform/aws/terraform.tf @@ -0,0 +1,53 @@ +# ============================================================================= +# TERRAFORM CONFIGURATION +# Provider and backend configuration for AWS deployment +# ============================================================================= + +terraform { + required_version = ">= 1.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + kubernetes = { + source = "hashicorp/kubernetes" + version = "~> 2.0" + } + random = { + source = "hashicorp/random" + version = "~> 3.0" + } + tls = { + source = "hashicorp/tls" + version = "~> 4.0" + } + } +} + +# ============================================================================= +# PROVIDER CONFIGURATION +# ============================================================================= +provider "aws" { + region = var.aws_region + + default_tags { + tags = { + Application = "Chartsmith" + ManagedBy = "Terraform" + Environment = var.environment_name + } + } +} + +# Data source for current AWS region +data "aws_region" "current" {} + +# Data source for current AWS account +data "aws_caller_identity" "current" {} + +# Data source for availability zones +data "aws_availability_zones" "available" { + state = "available" +} diff --git a/terraform/aws/variables.tf b/terraform/aws/variables.tf new file mode 100644 index 00000000..4254a357 --- /dev/null +++ b/terraform/aws/variables.tf @@ -0,0 +1,268 @@ +# ============================================================================= +# ENVIRONMENT IDENTIFICATION +# ============================================================================= +variable "environment_name" { + description = "Environment name for this Chartsmith deployment (e.g., 'production', 'staging', 'dev')" + type = string + + validation { + condition = can(regex("^[a-zA-Z0-9-]+$", var.environment_name)) + error_message = "Environment name must contain only letters, numbers, and hyphens." + } +} + +variable "aws_region" { + description = "AWS region for deployment" + type = string + default = "us-west-2" +} + +# ============================================================================= +# INFRASTRUCTURE CHOICES +# ============================================================================= +variable "create_vpc" { + description = "Create a new VPC for Chartsmith" + type = bool + default = true +} + +variable "create_eks_cluster" { + description = "Create a new EKS cluster for Chartsmith" + type = bool + default = true +} + +variable "create_database" { + description = "Create a new PostgreSQL database for Chartsmith" + type = bool + default = true +} + + +# ============================================================================= +# EXISTING INFRASTRUCTURE +# ============================================================================= +variable "existing_vpc_id" { + description = "ID of existing VPC (required if create_vpc = false)" + type = string + default = "" +} + +variable "existing_subnet_ids" { + description = "Private subnet IDs in existing VPC (required if create_vpc = false)" + type = list(string) + default = [] +} + +variable "existing_public_subnet_ids" { + description = "Public subnet IDs in existing VPC (required if create_vpc = false)" + type = list(string) + default = [] +} + +variable "existing_database_subnet_ids" { + description = "Database subnet IDs in existing VPC (optional - will use private subnets if not provided)" + type = list(string) + default = [] + + validation { + condition = length(var.existing_database_subnet_ids) == 0 || length(var.existing_database_subnet_ids) >= 2 + error_message = "When providing existing_database_subnet_ids, you must provide at least 2 subnet IDs in different availability zones." + } +} + +variable "existing_cluster_name" { + description = "Name of existing EKS cluster (required if create_eks_cluster = false)" + type = string + default = "" +} + +variable "existing_cluster_endpoint" { + description = "Endpoint of existing EKS cluster (required if create_eks_cluster = false)" + type = string + default = "" +} + +variable "existing_cluster_ca" { + description = "Certificate authority of existing EKS cluster (required if create_eks_cluster = false)" + type = string + default = "" +} + +variable "existing_oidc_issuer" { + description = "OIDC issuer URL of existing EKS cluster (required if create_eks_cluster = false)" + type = string + default = "" +} + +variable "existing_database_endpoint" { + description = "Endpoint of existing PostgreSQL database (required if create_database = false)" + type = string + default = "" +} + +# ============================================================================= +# VPC CONFIGURATION +# ============================================================================= +variable "vpc_cidr" { + description = "CIDR block for VPC" + type = string + default = "10.0.0.0/16" +} + +variable "availability_zones" { + description = "Availability zones to use (leave empty for automatic selection)" + type = list(string) + default = [] +} + +variable "private_subnet_cidrs" { + description = "CIDR blocks for private subnets" + type = list(string) + default = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"] +} + +variable "public_subnet_cidrs" { + description = "CIDR blocks for public subnets" + type = list(string) + default = ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"] +} + +variable "database_subnet_cidrs" { + description = "CIDR blocks for database subnets" + type = list(string) + default = ["10.0.201.0/24", "10.0.202.0/24", "10.0.203.0/24"] +} + +# ============================================================================= +# EKS CONFIGURATION +# ============================================================================= +variable "cluster_version" { + description = "Kubernetes version for EKS cluster" + type = string + default = "1.28" +} + +variable "node_groups" { + description = "EKS node group configurations" + type = map(object({ + instance_types = list(string) + min_size = number + max_size = number + desired_size = number + disk_size = number + ami_type = optional(string, "AL2_x86_64") + capacity_type = optional(string, "ON_DEMAND") + max_unavailable_percentage = optional(number, 25) + key_name = optional(string, "") + source_security_group_ids = optional(list(string), []) + launch_template_id = optional(string, "") + launch_template_version = optional(string, "$Latest") + labels = optional(map(string), {}) + taints = optional(list(object({ + key = string + value = string + effect = string + })), []) + })) + default = { + main = { + instance_types = ["t3.medium"] + min_size = 2 + max_size = 10 + desired_size = 3 + disk_size = 50 + } + } +} + +# ============================================================================= +# DATABASE CONFIGURATION +# ============================================================================= +variable "database_instance_class" { + description = "RDS instance class" + type = string + default = "db.t3.small" +} + +variable "database_allocated_storage" { + description = "Allocated storage for RDS instance (GB)" + type = number + default = 100 +} + +variable "database_engine_version" { + description = "PostgreSQL engine version" + type = string + default = "15.4" +} + +variable "database_name" { + description = "Name of the database to create" + type = string + default = "chartsmith" +} + +variable "database_username" { + description = "Username for database" + type = string + default = "chartsmith" +} + +# ============================================================================= +# SECURITY +# ============================================================================= +variable "allowed_cidr_blocks" { + description = "CIDR blocks allowed to access Chartsmith" + type = list(string) + default = ["0.0.0.0/0"] # Should be restricted in production +} + + +# ============================================================================= +# BACKUP CONFIGURATION +# ============================================================================= +variable "backup_retention_period" { + description = "Number of days to retain database backups" + type = number + default = 7 +} + +variable "backup_window" { + description = "Preferred backup window" + type = string + default = "03:00-04:00" +} + +variable "maintenance_window" { + description = "Preferred maintenance window" + type = string + default = "sun:04:00-sun:05:00" +} + +# ============================================================================= +# OPTIONAL FEATURES +# ============================================================================= +variable "enable_nat_gateway" { + description = "Enable NAT Gateway for private subnets" + type = bool + default = true +} + +variable "enable_vpn_gateway" { + description = "Enable VPN Gateway" + type = bool + default = false +} + +# ============================================================================= +# TAGS +# ============================================================================= +variable "tags" { + description = "Additional tags to apply to all resources" + type = map(string) + default = { + Application = "Chartsmith" + ManagedBy = "Terraform" + } +} diff --git a/terraform/azure/README.md b/terraform/azure/README.md new file mode 100644 index 00000000..63a77f5c --- /dev/null +++ b/terraform/azure/README.md @@ -0,0 +1,180 @@ +# Chartsmith Azure Infrastructure + +Create and manage Azure infrastructure for Chartsmith deployment. + +## Overview + +This Terraform configuration creates Azure infrastructure for Chartsmith with: + +- **Flexible Infrastructure**: Create new or use existing VNet, AKS, and database +- **Security First**: Private networking, Managed Identity, least privilege access +- **Production Ready**: Zone redundancy, automated backups, monitoring +- **Modular Design**: Use only the components you need + +## Quick Start + +1. **Choose Your Deployment Scenario**: + - [**Minimal**](examples/minimal/) - Create everything from scratch (recommended for new deployments) + - [**Existing VNet**](examples/existing-vnet/) - Use your existing Virtual Network infrastructure + - [**Existing Cluster**](examples/existing-cluster/) - Use your existing AKS cluster (coming soon) + +2. **Create Infrastructure**: + ```bash + cd examples/minimal # or your chosen example + cp terraform.tfvars.example terraform.tfvars + # Edit terraform.tfvars with your values + terraform init + terraform plan + terraform apply + ``` + +## Architecture + +### Current Implementation +``` +┌─────────────────────────────────────────────────────────────┐ +│ Azure Subscription │ +├─────────────────────────────────────────────────────────────┤ +│ Resource Group │ +│ ├── Virtual Network (Optional - can use existing) │ +│ │ ├── AKS Subnet │ +│ │ │ └── AKS Cluster + Node Pools ✅ │ +│ │ └── Database Subnet │ +│ │ └── Azure Database for PostgreSQL + pgvector ✅ │ +├─────────────────────────────────────────────────────────────┤ +│ Network Security Groups ✅ │ +│ ├── AKS NSG │ +│ └── Database NSG │ +├─────────────────────────────────────────────────────────────┤ +│ Key Vault ✅ │ +│ └── Database Connection String (if created) │ +└─────────────────────────────────────────────────────────────┘ +``` + +## Requirements + +### Azure Prerequisites + +1. **Azure CLI configured** with credentials +2. **Terraform installed** (>= 1.5.0) +3. **Sufficient Azure permissions**: + - `Network Contributor` (VNet, Subnets, NSG) + - `Azure Kubernetes Service Contributor` (AKS cluster) + - `SQL DB Contributor` (Azure Database for PostgreSQL) + - `Key Vault Administrator` (Key Vault for secrets) + - `Managed Identity Operator` (Service identities) + +4. **Azure subscription with sufficient quotas**: + - Virtual Networks: 1 additional (if creating new VNet) + - Azure Database for PostgreSQL instances: 1 additional + - AKS clusters: 1 additional + - Standard SKU Load Balancers: 1 additional + +## Deployment Scenarios + +### 🆕 New Deployment (Recommended) +Use the [minimal example](examples/minimal/) to create everything from scratch: +- New Virtual Network with proper subnets +- AKS cluster with managed node pools +- Azure Database for PostgreSQL with pgvector +- All network security groups + +**Best for**: First-time deployments, isolated environments + +### 🔄 Existing VNet +Use the [existing VNet example](examples/existing-vnet/) to create infrastructure within your current network: +- Reuse your VNet and subnets +- Create AKS cluster and database +- Integrate with existing infrastructure + +**Best for**: Adding Chartsmith to existing Azure environments + +### 🏗️ Existing Cluster (Coming Soon) +Create minimal additional infrastructure: +- Use existing AKS cluster +- Use existing database +- Create only necessary supporting resources + +**Best for**: Organizations with established Kubernetes platforms + +## Configuration + +### Basic Configuration +```hcl +# Environment identification +environment_name = "production" +azure_region = "eastus" +resource_group_name = "chartsmith-rg" + +# Infrastructure choices +create_vnet = true # Create new Virtual Network +create_database = true # Create Azure Database for PostgreSQL +``` + +### Security Configuration +```hcl +# Restrict access (recommended) +allowed_cidr_blocks = [ + "203.0.113.0/24" # Your office IP range +] + +# Database security +database_sku_name = "B_Standard_B2s" +database_high_availability_mode = "Disabled" # or "ZoneRedundant" for HA +``` + +## Monitoring & Operations + +### What's Monitored +- **Azure Database Performance**: CPU, connections, storage +- **Azure Monitor**: Application and database logs +- **Key Vault Access**: Secret usage tracking + +### Backup Strategy +- **Azure Database Automated Backups**: Configurable retention period +- **Point-in-time Recovery**: Available for databases +- **Geo-redundant Backups**: Optional for disaster recovery + +### Security Features +- **Encryption at Rest**: All storage encrypted by default +- **Encryption in Transit**: TLS for all communications +- **Private Networking**: Private endpoints for database +- **Managed Identity**: Secure service authentication for AKS +- **Least Privilege**: Minimal required permissions + +## Troubleshooting + +### Common Issues + +1. **Subscription Quota Exceeded** + ```bash + # Check quotas + az vm list-usage --location eastus --output table + ``` + +2. **Database Connection Issues** + ```bash + # Test database connectivity + az postgres flexible-server show --resource-group your-rg --name your-server + ``` + +3. **Permission Denied** + ```bash + # Verify Azure credentials + az account show + az account list-locations -o table + ``` + +## Development Status + +### ✅ Completed +- VNet module with subnets for AKS and database +- Azure Database for PostgreSQL with pgvector extension +- Network Security Groups for all components +- AKS cluster with Azure CNI and managed identities +- Example configurations + +### 🚧 Future Enhancements +- Managed Identity integration for service accounts +- Azure Application Gateway for ingress +- Additional monitoring and alerting diff --git a/terraform/azure/main.tf b/terraform/azure/main.tf new file mode 100644 index 00000000..638d2ede --- /dev/null +++ b/terraform/azure/main.tf @@ -0,0 +1,197 @@ +# ============================================================================= +# CHARTSMITH AZURE DEPLOYMENT +# Complete Azure infrastructure for self-hosted Chartsmith +# ============================================================================= + +locals { + name_prefix = "chartsmith-${var.environment_name}" + + common_tags = merge(var.tags, { + Application = "Chartsmith" + Environment = var.environment_name + ManagedBy = "Terraform" + CloudProvider = "Azure" + }) + + # Determine which network/subnets to use based on whether VNet is created or existing + vnet_id = var.create_vnet ? module.vnet[0].vnet_id : var.existing_vnet_id + vnet_name = var.create_vnet ? module.vnet[0].vnet_name : var.existing_vnet_name + aks_subnet_id = var.create_vnet ? module.vnet[0].aks_subnet_id : var.existing_aks_subnet_id + database_subnet_id = var.create_vnet ? module.vnet[0].database_subnet_id : var.existing_database_subnet_id +} + +# ============================================================================= +# RESOURCE GROUP +# ============================================================================= +resource "azurerm_resource_group" "main" { + name = var.resource_group_name + location = var.azure_region + + tags = local.common_tags +} + +# ============================================================================= +# VALIDATION CHECKS +# ============================================================================= +resource "terraform_data" "validate_existing_vnet_config" { + count = var.create_vnet ? 0 : 1 + + lifecycle { + precondition { + condition = var.existing_vnet_id != "" + error_message = "When using existing VNet (create_vnet = false), you must provide existing_vnet_id." + } + + precondition { + condition = var.existing_aks_subnet_id != "" + error_message = "When using existing VNet (create_vnet = false), you must provide existing_aks_subnet_id." + } + } +} + +# ============================================================================= +# VNET MODULE +# ============================================================================= +module "vnet" { + count = var.create_vnet ? 1 : 0 + source = "./modules/vnet" + + resource_group_name = azurerm_resource_group.main.name + location = azurerm_resource_group.main.location + name_prefix = local.name_prefix + + vnet_cidr = var.vnet_cidr + aks_subnet_cidr = var.aks_subnet_cidr + database_subnet_cidr = var.database_subnet_cidr + + tags = local.common_tags +} + +# ============================================================================= +# SECURITY (Network Security Groups) +# ============================================================================= +module "security" { + source = "./modules/security" + + resource_group_name = azurerm_resource_group.main.name + location = azurerm_resource_group.main.location + name_prefix = local.name_prefix + + allowed_cidr_blocks = var.allowed_cidr_blocks + + tags = local.common_tags + + depends_on = [module.vnet] +} + +# ============================================================================= +# KEY VAULT FOR SECRETS +# ============================================================================= +data "azurerm_client_config" "current" {} + +resource "azurerm_key_vault" "main" { + count = var.create_database ? 1 : 0 + + name = "${local.name_prefix}-kv" + location = azurerm_resource_group.main.location + resource_group_name = azurerm_resource_group.main.name + tenant_id = data.azurerm_client_config.current.tenant_id + sku_name = "standard" + + purge_protection_enabled = true + soft_delete_retention_days = 7 + + network_acls { + default_action = "Allow" + bypass = "AzureServices" + } + + tags = merge(local.common_tags, { + Component = "secrets" + }) +} + +resource "azurerm_key_vault_secret" "database_connection" { + count = var.create_database ? 1 : 0 + + name = "database-connection-string" + value = module.postgresql[0].connection_string + key_vault_id = azurerm_key_vault.main[0].id + + depends_on = [module.postgresql] +} + +# ============================================================================= +# AZURE DATABASE FOR POSTGRESQL MODULE +# ============================================================================= +module "postgresql" { + count = var.create_database ? 1 : 0 + source = "./modules/postgresql" + + resource_group_name = azurerm_resource_group.main.name + location = azurerm_resource_group.main.location + name_prefix = local.name_prefix + + # Instance configuration + sku_name = var.database_sku_name + server_version = var.database_version + storage_mb = var.database_storage_mb + + # Database configuration + database_name = var.database_name + admin_username = var.database_username + + # Network configuration + delegated_subnet_id = local.database_subnet_id + private_dns_zone_id = var.create_vnet ? module.vnet[0].private_dns_zone_id : var.existing_private_dns_zone_id + + # Backup configuration + backup_retention_days = var.backup_retention_days + geo_redundant_backup_enabled = var.geo_redundant_backup_enabled + + # High availability + high_availability_mode = var.database_high_availability_mode + + tags = local.common_tags + + depends_on = [module.vnet] +} + +# ============================================================================= +# AKS CLUSTER MODULE +# ============================================================================= +module "aks" { + count = var.create_aks_cluster ? 1 : 0 + source = "./modules/aks" + + resource_group_name = azurerm_resource_group.main.name + location = azurerm_resource_group.main.location + cluster_name = "${local.name_prefix}-aks" + + kubernetes_version = var.kubernetes_version + + vnet_subnet_id = local.aks_subnet_id + + # System node pool + default_node_pool = var.default_node_pool + + # Additional node pools + node_pools = var.node_pools + + # Identity + identity_type = "SystemAssigned" + + # Networking + network_plugin = "azure" + network_policy = "azure" + load_balancer_sku = "standard" + dns_service_ip = var.aks_dns_service_ip + service_cidr = var.aks_service_cidr + + # Logging + log_analytics_workspace_enabled = true + + tags = local.common_tags + + depends_on = [module.vnet, module.security] +} diff --git a/terraform/azure/modules/aks/main.tf b/terraform/azure/modules/aks/main.tf new file mode 100644 index 00000000..fa83c234 --- /dev/null +++ b/terraform/azure/modules/aks/main.tf @@ -0,0 +1,73 @@ +# AKS Cluster Module + +resource "azurerm_log_analytics_workspace" "main" { + count = var.log_analytics_workspace_enabled ? 1 : 0 + + name = "${var.cluster_name}-logs" + location = var.location + resource_group_name = var.resource_group_name + sku = "PerGB2018" + retention_in_days = 30 + + tags = var.tags +} + +resource "azurerm_kubernetes_cluster" "main" { + name = var.cluster_name + location = var.location + resource_group_name = var.resource_group_name + dns_prefix = var.cluster_name + kubernetes_version = var.kubernetes_version + + default_node_pool { + name = var.default_node_pool.name + vm_size = var.default_node_pool.vm_size + vnet_subnet_id = var.vnet_subnet_id + enable_auto_scaling = var.default_node_pool.enable_auto_scaling + node_count = var.default_node_pool.node_count + min_count = var.default_node_pool.min_count + max_count = var.default_node_pool.max_count + os_disk_size_gb = var.default_node_pool.os_disk_size_gb + } + + identity { + type = var.identity_type + } + + network_profile { + network_plugin = var.network_plugin + network_policy = var.network_policy + load_balancer_sku = var.load_balancer_sku + dns_service_ip = var.dns_service_ip + service_cidr = var.service_cidr + } + + dynamic "oms_agent" { + for_each = var.log_analytics_workspace_enabled ? [1] : [] + content { + log_analytics_workspace_id = azurerm_log_analytics_workspace.main[0].id + } + } + + tags = var.tags +} + +resource "azurerm_kubernetes_cluster_node_pool" "additional" { + for_each = var.node_pools + + name = each.key + kubernetes_cluster_id = azurerm_kubernetes_cluster.main.id + vm_size = each.value.vm_size + vnet_subnet_id = var.vnet_subnet_id + enable_auto_scaling = each.value.enable_auto_scaling + node_count = each.value.node_count + min_count = each.value.min_count + max_count = each.value.max_count + os_disk_size_gb = each.value.os_disk_size_gb + mode = each.value.mode + + node_labels = each.value.node_labels + node_taints = each.value.node_taints + + tags = var.tags +} diff --git a/terraform/azure/modules/aks/outputs.tf b/terraform/azure/modules/aks/outputs.tf new file mode 100644 index 00000000..0d616ca7 --- /dev/null +++ b/terraform/azure/modules/aks/outputs.tf @@ -0,0 +1,5 @@ +output "cluster_name" { value = azurerm_kubernetes_cluster.main.name } +output "cluster_id" { value = azurerm_kubernetes_cluster.main.id } +output "cluster_fqdn" { value = azurerm_kubernetes_cluster.main.fqdn } +output "cluster_version" { value = azurerm_kubernetes_cluster.main.kubernetes_version } +output "kube_config" { value = azurerm_kubernetes_cluster.main.kube_config_raw; sensitive = true } diff --git a/terraform/azure/modules/aks/variables.tf b/terraform/azure/modules/aks/variables.tf new file mode 100644 index 00000000..b100a353 --- /dev/null +++ b/terraform/azure/modules/aks/variables.tf @@ -0,0 +1,15 @@ +variable "resource_group_name" { type = string } +variable "location" { type = string } +variable "cluster_name" { type = string } +variable "kubernetes_version" { type = string } +variable "vnet_subnet_id" { type = string } +variable "default_node_pool" { type = any } +variable "node_pools" { type = any } +variable "identity_type" { type = string } +variable "network_plugin" { type = string } +variable "network_policy" { type = string } +variable "load_balancer_sku" { type = string } +variable "dns_service_ip" { type = string } +variable "service_cidr" { type = string } +variable "log_analytics_workspace_enabled" { type = bool } +variable "tags" { type = map(string); default = {} } diff --git a/terraform/azure/modules/postgresql/main.tf b/terraform/azure/modules/postgresql/main.tf new file mode 100644 index 00000000..2734e24b --- /dev/null +++ b/terraform/azure/modules/postgresql/main.tf @@ -0,0 +1,50 @@ +# Azure Database for PostgreSQL Module + +resource "random_password" "database_password" { + length = 32 + special = true + override_special = "!#$%&*()-_=+[]{}<>:?" +} + +resource "azurerm_postgresql_flexible_server" "main" { + name = "${var.name_prefix}-postgres" + resource_group_name = var.resource_group_name + location = var.location + + version = var.server_version + sku_name = var.sku_name + storage_mb = var.storage_mb + + administrator_login = var.admin_username + administrator_password = random_password.database_password.result + + delegated_subnet_id = var.delegated_subnet_id + private_dns_zone_id = var.private_dns_zone_id + + backup_retention_days = var.backup_retention_days + geo_redundant_backup_enabled = var.geo_redundant_backup_enabled + + dynamic "high_availability" { + for_each = var.high_availability_mode != "Disabled" ? [1] : [] + content { + mode = var.high_availability_mode + } + } + + tags = var.tags + + depends_on = [var.private_dns_zone_id] +} + +resource "azurerm_postgresql_flexible_server_database" "main" { + name = var.database_name + server_id = azurerm_postgresql_flexible_server.main.id + charset = "UTF8" + collation = "en_US.utf8" +} + +resource "azurerm_postgresql_flexible_server_configuration" "pgvector" { + name = "azure.extensions" + server_id = azurerm_postgresql_flexible_server.main.id + value = "VECTOR" +} diff --git a/terraform/azure/modules/postgresql/outputs.tf b/terraform/azure/modules/postgresql/outputs.tf new file mode 100644 index 00000000..f3b2daf6 --- /dev/null +++ b/terraform/azure/modules/postgresql/outputs.tf @@ -0,0 +1,9 @@ +output "server_name" { value = azurerm_postgresql_flexible_server.main.name } +output "server_fqdn" { value = azurerm_postgresql_flexible_server.main.fqdn } +output "database_name" { value = azurerm_postgresql_flexible_server_database.main.name } +output "admin_username" { value = azurerm_postgresql_flexible_server.main.administrator_login } +output "admin_password" { value = random_password.database_password.result; sensitive = true } +output "connection_string" { + value = "postgresql://${azurerm_postgresql_flexible_server.main.administrator_login}:${random_password.database_password.result}@${azurerm_postgresql_flexible_server.main.fqdn}:5432/${azurerm_postgresql_flexible_server_database.main.name}" + sensitive = true +} diff --git a/terraform/azure/modules/postgresql/variables.tf b/terraform/azure/modules/postgresql/variables.tf new file mode 100644 index 00000000..17a7c6ee --- /dev/null +++ b/terraform/azure/modules/postgresql/variables.tf @@ -0,0 +1,14 @@ +variable "resource_group_name" { type = string } +variable "location" { type = string } +variable "name_prefix" { type = string } +variable "sku_name" { type = string } +variable "server_version" { type = string } +variable "storage_mb" { type = number } +variable "database_name" { type = string } +variable "admin_username" { type = string } +variable "delegated_subnet_id" { type = string } +variable "private_dns_zone_id" { type = string } +variable "backup_retention_days" { type = number } +variable "geo_redundant_backup_enabled" { type = bool } +variable "high_availability_mode" { type = string } +variable "tags" { type = map(string); default = {} } diff --git a/terraform/azure/modules/security/main.tf b/terraform/azure/modules/security/main.tf new file mode 100644 index 00000000..5708cc86 --- /dev/null +++ b/terraform/azure/modules/security/main.tf @@ -0,0 +1,53 @@ +# Azure Network Security Groups Module + +resource "azurerm_network_security_group" "aks" { + name = "${var.name_prefix}-aks-nsg" + location = var.location + resource_group_name = var.resource_group_name + + security_rule { + name = "allow-https" + priority = 100 + direction = "Inbound" + access = "Allow" + protocol = "Tcp" + source_port_range = "*" + destination_port_range = "443" + source_address_prefixes = var.allowed_cidr_blocks + destination_address_prefix = "*" + } + + security_rule { + name = "allow-http" + priority = 110 + direction = "Inbound" + access = "Allow" + protocol = "Tcp" + source_port_range = "*" + destination_port_range = "80" + source_address_prefixes = var.allowed_cidr_blocks + destination_address_prefix = "*" + } + + tags = var.tags +} + +resource "azurerm_network_security_group" "database" { + name = "${var.name_prefix}-database-nsg" + location = var.location + resource_group_name = var.resource_group_name + + security_rule { + name = "allow-postgresql" + priority = 100 + direction = "Inbound" + access = "Allow" + protocol = "Tcp" + source_port_range = "*" + destination_port_range = "5432" + source_address_prefix = "VirtualNetwork" + destination_address_prefix = "*" + } + + tags = var.tags +} diff --git a/terraform/azure/modules/security/outputs.tf b/terraform/azure/modules/security/outputs.tf new file mode 100644 index 00000000..4e368ad6 --- /dev/null +++ b/terraform/azure/modules/security/outputs.tf @@ -0,0 +1,2 @@ +output "aks_nsg_id" { value = azurerm_network_security_group.aks.id } +output "database_nsg_id" { value = azurerm_network_security_group.database.id } diff --git a/terraform/azure/modules/security/variables.tf b/terraform/azure/modules/security/variables.tf new file mode 100644 index 00000000..b764461f --- /dev/null +++ b/terraform/azure/modules/security/variables.tf @@ -0,0 +1,5 @@ +variable "resource_group_name" { type = string } +variable "location" { type = string } +variable "name_prefix" { type = string } +variable "allowed_cidr_blocks" { type = list(string) } +variable "tags" { type = map(string); default = {} } diff --git a/terraform/azure/modules/vnet/main.tf b/terraform/azure/modules/vnet/main.tf new file mode 100644 index 00000000..5b1c767d --- /dev/null +++ b/terraform/azure/modules/vnet/main.tf @@ -0,0 +1,50 @@ +# Azure Virtual Network Module + +resource "azurerm_virtual_network" "main" { + name = "${var.name_prefix}-vnet" + location = var.location + resource_group_name = var.resource_group_name + address_space = [var.vnet_cidr] + + tags = var.tags +} + +resource "azurerm_subnet" "aks" { + name = "${var.name_prefix}-aks-subnet" + resource_group_name = var.resource_group_name + virtual_network_name = azurerm_virtual_network.main.name + address_prefixes = [var.aks_subnet_cidr] +} + +resource "azurerm_subnet" "database" { + name = "${var.name_prefix}-database-subnet" + resource_group_name = var.resource_group_name + virtual_network_name = azurerm_virtual_network.main.name + address_prefixes = [var.database_subnet_cidr] + + delegation { + name = "postgresql-delegation" + service_delegation { + name = "Microsoft.DBforPostgreSQL/flexibleServers" + actions = [ + "Microsoft.Network/virtualNetworks/subnets/join/action" + ] + } + } +} + +resource "azurerm_private_dns_zone" "postgresql" { + name = "${var.name_prefix}.private.postgres.database.azure.com" + resource_group_name = var.resource_group_name + + tags = var.tags +} + +resource "azurerm_private_dns_zone_virtual_network_link" "postgresql" { + name = "${var.name_prefix}-postgresql-link" + private_dns_zone_name = azurerm_private_dns_zone.postgresql.name + resource_group_name = var.resource_group_name + virtual_network_id = azurerm_virtual_network.main.id + + tags = var.tags +} diff --git a/terraform/azure/modules/vnet/outputs.tf b/terraform/azure/modules/vnet/outputs.tf new file mode 100644 index 00000000..066a1bf7 --- /dev/null +++ b/terraform/azure/modules/vnet/outputs.tf @@ -0,0 +1,5 @@ +output "vnet_id" { value = azurerm_virtual_network.main.id } +output "vnet_name" { value = azurerm_virtual_network.main.name } +output "aks_subnet_id" { value = azurerm_subnet.aks.id } +output "database_subnet_id" { value = azurerm_subnet.database.id } +output "private_dns_zone_id" { value = azurerm_private_dns_zone.postgresql.id } diff --git a/terraform/azure/modules/vnet/variables.tf b/terraform/azure/modules/vnet/variables.tf new file mode 100644 index 00000000..74689c37 --- /dev/null +++ b/terraform/azure/modules/vnet/variables.tf @@ -0,0 +1,7 @@ +variable "resource_group_name" { type = string } +variable "location" { type = string } +variable "name_prefix" { type = string } +variable "vnet_cidr" { type = string } +variable "aks_subnet_cidr" { type = string } +variable "database_subnet_cidr" { type = string } +variable "tags" { type = map(string); default = {} } diff --git a/terraform/azure/outputs.tf b/terraform/azure/outputs.tf new file mode 100644 index 00000000..44d0b2fc --- /dev/null +++ b/terraform/azure/outputs.tf @@ -0,0 +1,144 @@ +# ============================================================================= +# AZURE DEPLOYMENT OUTPUTS +# Information customers get after successful deployment +# ============================================================================= + +# ============================================================================= +# DEPLOYMENT INFORMATION +# ============================================================================= +output "deployment_info" { + description = "Summary of deployment information" + value = { + environment_name = var.environment_name + azure_region = var.azure_region + resource_group_name = azurerm_resource_group.main.name + vnet_created = var.create_vnet + aks_created = var.create_aks_cluster + database_created = var.create_database + cluster_name = var.create_aks_cluster ? module.aks[0].cluster_name : var.existing_cluster_name + } +} + +# ============================================================================= +# NETWORK INFORMATION +# ============================================================================= +output "vnet_info" { + description = "Virtual Network information" + value = var.create_vnet ? { + vnet_id = module.vnet[0].vnet_id + vnet_name = module.vnet[0].vnet_name + vnet_cidr = var.vnet_cidr + aks_subnet_id = module.vnet[0].aks_subnet_id + database_subnet_id = module.vnet[0].database_subnet_id + } : { + vnet_id = var.existing_vnet_id + message = "Using existing Virtual Network" + } +} + +# ============================================================================= +# DATABASE INFORMATION +# ============================================================================= +output "database_info" { + description = "Azure Database for PostgreSQL connection information" + value = var.create_database ? { + server_name = module.postgresql[0].server_name + server_fqdn = module.postgresql[0].server_fqdn + database_name = module.postgresql[0].database_name + server_version = var.database_version + sku_name = var.database_sku_name + storage_mb = var.database_storage_mb + } : { + server_name = var.existing_database_server + message = "Using existing PostgreSQL server" + } + sensitive = true +} + +# ============================================================================= +# AKS CLUSTER INFORMATION +# ============================================================================= +output "aks_info" { + description = "AKS cluster information" + value = var.create_aks_cluster ? { + cluster_name = module.aks[0].cluster_name + cluster_fqdn = module.aks[0].cluster_fqdn + cluster_version = module.aks[0].cluster_version + node_pools = keys(var.node_pools) + kubectl_config = "az aks get-credentials --resource-group ${azurerm_resource_group.main.name} --name ${module.aks[0].cluster_name}" + } : { + cluster_name = var.existing_cluster_name + message = "Using existing AKS cluster" + } +} + +# ============================================================================= +# SECRETS INFORMATION +# ============================================================================= +output "secrets_info" { + description = "Information about created secrets" + value = { + key_vault_name = var.create_database ? azurerm_key_vault.main[0].name : null + secrets_created = var.create_database ? ["database-connection-string"] : [] + resource_group = azurerm_resource_group.main.name + } +} + +# ============================================================================= +# NEXT STEPS FOR CUSTOMER +# ============================================================================= +output "next_steps" { + description = "What to do after deployment" + value = <<-EOT + ✅ Chartsmith Azure infrastructure deployed successfully! + + 📋 What was created: + ${var.create_vnet ? "- Virtual Network with subnets for AKS and database" : "- Used existing Virtual Network"} + ${var.create_aks_cluster ? "- AKS cluster with managed node pools and Azure CNI" : "- Configured for existing AKS cluster"} + ${var.create_database ? "- Azure Database for PostgreSQL with pgvector extension" : "- Configured for existing database"} + - Network Security Groups for AKS and database + ${var.create_database ? "- Database connection string in Key Vault" : ""} + + 📚 Resources: + - Resource Group: ${azurerm_resource_group.main.name} + - Virtual Network: ${local.vnet_name} + ${var.create_aks_cluster ? "- AKS Cluster: ${module.aks[0].cluster_name}" : "- AKS Cluster: ${var.existing_cluster_name} (existing)"} + - Database: ${var.create_database ? module.postgresql[0].server_fqdn : var.existing_database_server} + ${var.create_database ? "- Key Vault: ${azurerm_key_vault.main[0].name}" : ""} + + 🔧 Manual steps required: + 1. Configure kubectl access: ${var.create_aks_cluster ? "az aks get-credentials --resource-group ${azurerm_resource_group.main.name} --name ${module.aks[0].cluster_name}" : "az aks get-credentials --name ${var.existing_cluster_name}"} + 2. Deploy Chartsmith via Helm + 3. Verify database connectivity and pgvector extension + + EOT +} + +# ============================================================================= +# TERRAFORM STATE INFORMATION +# ============================================================================= +output "terraform_info" { + description = "Terraform state information" + value = { + terraform_version = "~> 1.5" + azurerm_provider_version = "~> 3.0" + resource_group = azurerm_resource_group.main.name + region = var.azure_region + state_backend = "Configure remote state backend for production use" + } +} + +# ============================================================================= +# SENSITIVE OUTPUTS (for automation) +# ============================================================================= +output "database_connection_string" { + description = "Database connection string" + value = var.create_database ? module.postgresql[0].connection_string : "Configure manually for existing database" + sensitive = true +} + +output "key_vault_uri" { + description = "URI of Key Vault" + value = var.create_database ? azurerm_key_vault.main[0].vault_uri : null + sensitive = true +} diff --git a/terraform/azure/terraform.tf b/terraform/azure/terraform.tf new file mode 100644 index 00000000..6320fb6e --- /dev/null +++ b/terraform/azure/terraform.tf @@ -0,0 +1,25 @@ +terraform { + required_version = ">= 1.5.0" + + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = "~> 3.0" + } + random = { + source = "hashicorp/random" + version = "~> 3.5" + } + } +} + +provider "azurerm" { + features { + key_vault { + purge_soft_delete_on_destroy = false + } + resource_group { + prevent_deletion_if_contains_resources = false + } + } +} diff --git a/terraform/azure/variables.tf b/terraform/azure/variables.tf new file mode 100644 index 00000000..08afb578 --- /dev/null +++ b/terraform/azure/variables.tf @@ -0,0 +1,244 @@ +# ============================================================================= +# ENVIRONMENT IDENTIFICATION +# ============================================================================= +variable "environment_name" { + description = "Environment name for this Chartsmith deployment (e.g., 'production', 'staging', 'dev')" + type = string + + validation { + condition = can(regex("^[a-z0-9-]+$", var.environment_name)) + error_message = "Environment name must contain only lowercase letters, numbers, and hyphens." + } +} + +variable "azure_region" { + description = "Azure region for deployment" + type = string + default = "eastus" +} + +variable "resource_group_name" { + description = "Name of the Azure resource group" + type = string +} + +# ============================================================================= +# INFRASTRUCTURE CHOICES +# ============================================================================= +variable "create_vnet" { + description = "Create a new Virtual Network for Chartsmith" + type = bool + default = true +} + +variable "create_aks_cluster" { + description = "Create a new AKS cluster for Chartsmith" + type = bool + default = true +} + +variable "create_database" { + description = "Create a new Azure Database for PostgreSQL for Chartsmith" + type = bool + default = true +} + +# ============================================================================= +# EXISTING INFRASTRUCTURE +# ============================================================================= +variable "existing_vnet_id" { + description = "ID of existing Virtual Network (required if create_vnet = false)" + type = string + default = "" +} + +variable "existing_vnet_name" { + description = "Name of existing Virtual Network (required if create_vnet = false)" + type = string + default = "" +} + +variable "existing_aks_subnet_id" { + description = "Subnet ID for AKS in existing VNet (required if create_vnet = false)" + type = string + default = "" +} + +variable "existing_database_subnet_id" { + description = "Subnet ID for database in existing VNet (required if create_vnet = false)" + type = string + default = "" +} + +variable "existing_private_dns_zone_id" { + description = "Private DNS zone ID for PostgreSQL (required if create_vnet = false)" + type = string + default = "" +} + +variable "existing_cluster_name" { + description = "Name of existing AKS cluster (required if create_aks_cluster = false)" + type = string + default = "" +} + +variable "existing_database_server" { + description = "Name of existing PostgreSQL server (required if create_database = false)" + type = string + default = "" +} + +# ============================================================================= +# VNET CONFIGURATION +# ============================================================================= +variable "vnet_cidr" { + description = "CIDR block for Virtual Network" + type = string + default = "10.0.0.0/16" +} + +variable "aks_subnet_cidr" { + description = "CIDR block for AKS subnet" + type = string + default = "10.0.1.0/24" +} + +variable "database_subnet_cidr" { + description = "CIDR block for database subnet" + type = string + default = "10.0.2.0/24" +} + +# ============================================================================= +# AKS CONFIGURATION +# ============================================================================= +variable "kubernetes_version" { + description = "Kubernetes version for AKS cluster" + type = string + default = "1.28" +} + +variable "default_node_pool" { + description = "Default node pool configuration" + type = object({ + name = string + vm_size = string + node_count = number + min_count = number + max_count = number + enable_auto_scaling = bool + os_disk_size_gb = number + }) + default = { + name = "system" + vm_size = "Standard_D2s_v3" + node_count = 3 + min_count = 2 + max_count = 10 + enable_auto_scaling = true + os_disk_size_gb = 50 + } +} + +variable "node_pools" { + description = "Additional node pool configurations" + type = map(object({ + vm_size = string + node_count = number + min_count = number + max_count = number + enable_auto_scaling = bool + os_disk_size_gb = number + mode = optional(string, "User") + node_labels = optional(map(string), {}) + node_taints = optional(list(string), []) + })) + default = {} +} + +variable "aks_service_cidr" { + description = "CIDR for Kubernetes services" + type = string + default = "10.1.0.0/16" +} + +variable "aks_dns_service_ip" { + description = "IP address for Kubernetes DNS service" + type = string + default = "10.1.0.10" +} + +# ============================================================================= +# DATABASE CONFIGURATION +# ============================================================================= +variable "database_sku_name" { + description = "Azure Database for PostgreSQL SKU" + type = string + default = "B_Standard_B2s" +} + +variable "database_version" { + description = "PostgreSQL version" + type = string + default = "15" +} + +variable "database_storage_mb" { + description = "Storage size for database (MB)" + type = number + default = 32768 +} + +variable "database_name" { + description = "Name of the database to create" + type = string + default = "chartsmith" +} + +variable "database_username" { + description = "Admin username for database" + type = string + default = "chartsmith" +} + +variable "database_high_availability_mode" { + description = "High availability mode (ZoneRedundant or SameZone)" + type = string + default = "Disabled" +} + +# ============================================================================= +# SECURITY +# ============================================================================= +variable "allowed_cidr_blocks" { + description = "CIDR blocks allowed to access Chartsmith" + type = list(string) + default = ["0.0.0.0/0"] +} + +# ============================================================================= +# BACKUP CONFIGURATION +# ============================================================================= +variable "backup_retention_days" { + description = "Number of days to retain database backups" + type = number + default = 7 +} + +variable "geo_redundant_backup_enabled" { + description = "Enable geo-redundant backups" + type = bool + default = false +} + +# ============================================================================= +# TAGS +# ============================================================================= +variable "tags" { + description = "Additional tags to apply to all resources" + type = map(string) + default = { + Application = "Chartsmith" + ManagedBy = "Terraform" + } +} diff --git a/terraform/gcp/README.md b/terraform/gcp/README.md new file mode 100644 index 00000000..d84f9c49 --- /dev/null +++ b/terraform/gcp/README.md @@ -0,0 +1,194 @@ +# Chartsmith GCP Infrastructure + +Create and manage GCP infrastructure for Chartsmith deployment. + +## Overview + +This Terraform configuration creates GCP infrastructure for Chartsmith with: + +- **Flexible Infrastructure**: Create new or use existing VPC, GKE, and database +- **Security First**: Private networking, Workload Identity, least privilege access +- **Production Ready**: Regional availability, automated backups, monitoring +- **Modular Design**: Use only the components you need + +## Quick Start + +1. **Choose Your Deployment Scenario**: + - [**Minimal**](examples/minimal/) - Create everything from scratch (recommended for new deployments) + - [**Existing VPC**](examples/existing-vpc/) - Use your existing VPC infrastructure + - [**Existing Cluster**](examples/existing-cluster/) - Use your existing GKE cluster (coming soon) + +2. **Create Infrastructure**: + ```bash + cd examples/minimal # or your chosen example + cp terraform.tfvars.example terraform.tfvars + # Edit terraform.tfvars with your values + terraform init + terraform plan + terraform apply + ``` + +## Architecture + +### Current Implementation +``` +┌─────────────────────────────────────────────────────────────┐ +│ GCP Project │ +├─────────────────────────────────────────────────────────────┤ +│ VPC Network (Optional - can use existing) │ +│ ├── Subnet with Secondary Ranges │ +│ │ ├── Primary: Nodes │ +│ │ ├── Secondary: Pods │ +│ │ └── Secondary: Services │ +│ │ └── GKE Cluster + Node Pools ✅ │ +│ └── Private Service Connection │ +│ └── Cloud SQL PostgreSQL + pgvector ✅ │ +├─────────────────────────────────────────────────────────────┤ +│ Firewall Rules ✅ │ +│ ├── GKE Control Plane │ +│ ├── GKE Nodes │ +│ └── Cloud SQL │ +├─────────────────────────────────────────────────────────────┤ +│ Secret Manager ✅ │ +│ └── Database Credentials (if created) │ +└─────────────────────────────────────────────────────────────┘ +``` + +## Requirements + +### GCP Prerequisites + +1. **gcloud CLI configured** with credentials +2. **Terraform installed** (>= 1.5.0) +3. **Sufficient GCP permissions**: + - `Compute Network Admin` (VPC, Subnets, Firewall) + - `Kubernetes Engine Admin` (GKE cluster) + - `Cloud SQL Admin` (PostgreSQL database) + - `Secret Manager Admin` (API key storage) + - `Service Account Admin` (Service accounts and IAM) + +4. **APIs enabled in your GCP project**: + - Compute Engine API + - Kubernetes Engine API + - Cloud SQL Admin API + - Secret Manager API + - Service Networking API + +### GCP Resource Quotas + +Ensure your project has sufficient quotas: +- **VPCs**: 1 additional (if creating new VPC) +- **Cloud SQL instances**: 1 additional +- **GKE clusters**: 1 additional + +## Deployment Scenarios + +### 🆕 New Deployment (Recommended) +Use the [minimal example](examples/minimal/) to create everything from scratch: +- New VPC with proper networking +- GKE cluster with managed node pools +- Cloud SQL PostgreSQL with pgvector +- All firewall rules + +**Best for**: First-time deployments, isolated environments + +### 🔄 Existing VPC +Use the [existing VPC example](examples/existing-vpc/) to create infrastructure within your current network: +- Reuse your VPC and subnets +- Create GKE cluster and database +- Integrate with existing infrastructure + +**Best for**: Adding Chartsmith to existing GCP environments + +### 🏗️ Existing Cluster (Coming Soon) +Create minimal additional infrastructure: +- Use existing GKE cluster +- Use existing database +- Create only necessary supporting resources + +**Best for**: Organizations with established Kubernetes platforms + +## Configuration + +### Basic Configuration +```hcl +# Environment identification +environment_name = "production" +gcp_project = "your-project-id" +gcp_region = "us-central1" + +# Infrastructure choices +create_vpc = true # Create new VPC +create_database = true # Create Cloud SQL PostgreSQL database +``` + +### Security Configuration +```hcl +# Restrict access (recommended) +allowed_cidr_blocks = [ + "203.0.113.0/24" # Your office IP range +] + +# Database security +database_tier = "db-custom-2-7680" +database_availability_type = "ZONAL" # or "REGIONAL" for HA +``` + +## Monitoring & Operations + +### What's Monitored +- **Cloud SQL Performance**: CPU, connections, storage +- **Cloud Logging**: Application and database logs +- **Secret Manager Access**: API key usage tracking + +### Backup Strategy +- **Cloud SQL Automated Backups**: Daily backups with point-in-time recovery +- **Secret Manager Versioning**: Automatic secret versioning + +### Security Features +- **Encryption at Rest**: All storage encrypted by default +- **Encryption in Transit**: TLS for all communications +- **Private Networking**: Private IP for Cloud SQL +- **Workload Identity**: Secure service account authentication for GKE +- **Least Privilege**: Minimal required permissions + +## Troubleshooting + +### Common Issues + +1. **API Not Enabled** + ```bash + # Enable required APIs + gcloud services enable compute.googleapis.com + gcloud services enable container.googleapis.com + gcloud services enable sqladmin.googleapis.com + gcloud services enable secretmanager.googleapis.com + gcloud services enable servicenetworking.googleapis.com + ``` + +2. **Database Connection Issues** + ```bash + # Test database connectivity + gcloud sql instances describe your-instance-name + ``` + +3. **Permission Denied** + ```bash + # Verify GCP credentials + gcloud auth list + gcloud config get-value project + ``` + +## Development Status + +### ✅ Completed +- VPC module with secondary ranges for GKE +- Cloud SQL PostgreSQL with pgvector extension +- Firewall rules for all components +- GKE cluster with Workload Identity +- Example configurations + +### 🚧 Future Enhancements +- IAM roles and service accounts for Workload Identity +- Cloud Armor for DDoS protection +- Additional monitoring and alerting diff --git a/terraform/gcp/main.tf b/terraform/gcp/main.tf new file mode 100644 index 00000000..296c8bc7 --- /dev/null +++ b/terraform/gcp/main.tf @@ -0,0 +1,186 @@ +# ============================================================================= +# CHARTSMITH GCP DEPLOYMENT +# Complete GCP infrastructure for self-hosted Chartsmith +# ============================================================================= + +locals { + name_prefix = "chartsmith-${var.environment_name}" + + common_labels = merge(var.labels, { + application = "chartsmith" + environment = var.environment_name + managed_by = "terraform" + cloud_provider = "gcp" + }) + + # Determine which network/subnets to use based on whether VPC is created or existing + network_name = var.create_vpc ? module.vpc[0].network_name : var.existing_network_name + subnet_name = var.create_vpc ? module.vpc[0].subnet_name : var.existing_subnet_name + pods_range_name = var.create_vpc ? module.vpc[0].pods_range_name : var.existing_pods_range_name + services_range_name = var.create_vpc ? module.vpc[0].services_range_name : var.existing_services_range_name +} + +# ============================================================================= +# VALIDATION CHECKS +# ============================================================================= +resource "terraform_data" "validate_existing_vpc_config" { + count = var.create_vpc ? 0 : 1 + + lifecycle { + precondition { + condition = var.existing_network_name != "" + error_message = "When using existing VPC (create_vpc = false), you must provide existing_network_name." + } + + precondition { + condition = var.existing_subnet_name != "" + error_message = "When using existing VPC (create_vpc = false), you must provide existing_subnet_name." + } + } +} + +# ============================================================================= +# VPC MODULE +# ============================================================================= +module "vpc" { + count = var.create_vpc ? 1 : 0 + source = "./modules/vpc" + + project_id = var.gcp_project + name_prefix = local.name_prefix + region = var.gcp_region + + subnet_cidr = var.subnet_cidr + pods_secondary_range = var.pods_secondary_range + services_secondary_range = var.services_secondary_range + + enable_nat_gateway = var.enable_nat_gateway + + labels = local.common_labels +} + +# ============================================================================= +# SECURITY (Firewall Rules) +# ============================================================================= +module "security" { + source = "./modules/security" + + project_id = var.gcp_project + network_name = local.network_name + name_prefix = local.name_prefix + + allowed_cidr_blocks = var.allowed_cidr_blocks + + labels = local.common_labels + + depends_on = [module.vpc] +} + +# ============================================================================= +# DATABASE CREDENTIALS SECRET +# ============================================================================= +resource "google_secret_manager_secret" "database_credentials" { + count = var.create_database ? 1 : 0 + + project = var.gcp_project + secret_id = "${local.name_prefix}-database-credentials" + + replication { + auto {} + } + + labels = merge(local.common_labels, { + component = "database" + }) +} + +resource "google_secret_manager_secret_version" "database_credentials" { + count = var.create_database ? 1 : 0 + + secret = google_secret_manager_secret.database_credentials[0].id + secret_data = jsonencode({ + host = module.cloudsql[0].private_ip_address + port = 5432 + database = module.cloudsql[0].database_name + username = module.cloudsql[0].username + password = module.cloudsql[0].password + connection_string = module.cloudsql[0].connection_string + }) + + depends_on = [module.cloudsql] +} + +# ============================================================================= +# CLOUD SQL POSTGRESQL MODULE +# ============================================================================= +module "cloudsql" { + count = var.create_database ? 1 : 0 + source = "./modules/cloudsql" + + project_id = var.gcp_project + name_prefix = local.name_prefix + region = var.gcp_region + + # Instance configuration + tier = var.database_tier + database_version = var.database_version + availability_type = var.database_availability_type + disk_size = var.database_disk_size + disk_autoresize = true + disk_autoresize_limit = var.database_disk_autoresize_limit + + # Database configuration + database_name = var.database_name + username = var.database_username + + # Network configuration + network_id = local.network_name + private_network = var.create_vpc ? module.vpc[0].network_id : var.existing_network_id + + # Backup configuration + backup_enabled = true + backup_start_time = var.backup_start_time + point_in_time_recovery_enabled = true + + # Security + deletion_protection = true + + labels = local.common_labels + + depends_on = [module.vpc] +} + +# ============================================================================= +# GKE CLUSTER MODULE +# ============================================================================= +module "gke" { + count = var.create_gke_cluster ? 1 : 0 + source = "./modules/gke" + + project_id = var.gcp_project + cluster_name = "${local.name_prefix}-gke" + region = var.gcp_region + + network_name = local.network_name + subnet_name = local.subnet_name + pods_secondary_range_name = local.pods_range_name + services_secondary_range_name = local.services_range_name + + # Kubernetes version + kubernetes_version = var.kubernetes_version + + # Node pools configuration + node_pools = var.node_pools + + # Logging and monitoring + logging_service = "logging.googleapis.com/kubernetes" + monitoring_service = "monitoring.googleapis.com/kubernetes" + + # Security + enable_workload_identity = true + enable_shielded_nodes = true + + labels = local.common_labels + + depends_on = [module.vpc, module.security] +} diff --git a/terraform/gcp/modules/cloudsql/main.tf b/terraform/gcp/modules/cloudsql/main.tf new file mode 100644 index 00000000..175ced94 --- /dev/null +++ b/terraform/gcp/modules/cloudsql/main.tf @@ -0,0 +1,55 @@ +# Cloud SQL PostgreSQL Module + +resource "random_password" "database_password" { + length = 32 + special = true + override_special = "!#$%&*()-_=+[]{}<>:?" +} + +resource "google_sql_database_instance" "main" { + name = "${var.name_prefix}-postgres" + project = var.project_id + region = var.region + database_version = var.database_version + + deletion_protection = var.deletion_protection + + settings { + tier = var.tier + availability_type = var.availability_type + disk_size = var.disk_size + disk_autoresize = var.disk_autoresize + disk_autoresize_limit = var.disk_autoresize_limit + + ip_configuration { + ipv4_enabled = false + private_network = var.private_network + require_ssl = true + } + + backup_configuration { + enabled = var.backup_enabled + start_time = var.backup_start_time + point_in_time_recovery_enabled = var.point_in_time_recovery_enabled + transaction_log_retention_days = 7 + } + + database_flags { + name = "cloudsql.enable_pgvector" + value = "on" + } + } +} + +resource "google_sql_database" "main" { + name = var.database_name + project = var.project_id + instance = google_sql_database_instance.main.name +} + +resource "google_sql_user" "main" { + name = var.username + project = var.project_id + instance = google_sql_database_instance.main.name + password = random_password.database_password.result +} diff --git a/terraform/gcp/modules/cloudsql/outputs.tf b/terraform/gcp/modules/cloudsql/outputs.tf new file mode 100644 index 00000000..4f28b9b4 --- /dev/null +++ b/terraform/gcp/modules/cloudsql/outputs.tf @@ -0,0 +1,11 @@ +output "instance_name" { value = google_sql_database_instance.main.name } +output "connection_name" { value = google_sql_database_instance.main.connection_name } +output "private_ip_address" { value = google_sql_database_instance.main.private_ip_address } +output "database_name" { value = google_sql_database.main.name } +output "database_version" { value = google_sql_database_instance.main.database_version } +output "username" { value = google_sql_user.main.name } +output "password" { value = random_password.database_password.result; sensitive = true } +output "connection_string" { + value = "postgresql://${google_sql_user.main.name}:${random_password.database_password.result}@${google_sql_database_instance.main.private_ip_address}:5432/${google_sql_database.main.name}" + sensitive = true +} diff --git a/terraform/gcp/modules/cloudsql/variables.tf b/terraform/gcp/modules/cloudsql/variables.tf new file mode 100644 index 00000000..78501eb0 --- /dev/null +++ b/terraform/gcp/modules/cloudsql/variables.tf @@ -0,0 +1,18 @@ +variable "project_id" { type = string } +variable "name_prefix" { type = string } +variable "region" { type = string } +variable "tier" { type = string } +variable "database_version" { type = string } +variable "availability_type" { type = string } +variable "disk_size" { type = number } +variable "disk_autoresize" { type = bool } +variable "disk_autoresize_limit" { type = number } +variable "database_name" { type = string } +variable "username" { type = string } +variable "network_id" { type = string } +variable "private_network" { type = string } +variable "backup_enabled" { type = bool } +variable "backup_start_time" { type = string } +variable "point_in_time_recovery_enabled" { type = bool } +variable "deletion_protection" { type = bool } +variable "labels" { type = map(string); default = {} } diff --git a/terraform/gcp/modules/gke/main.tf b/terraform/gcp/modules/gke/main.tf new file mode 100644 index 00000000..6347f281 --- /dev/null +++ b/terraform/gcp/modules/gke/main.tf @@ -0,0 +1,85 @@ +# GKE Cluster Module + +resource "google_container_cluster" "main" { + name = var.cluster_name + project = var.project_id + location = var.region + + remove_default_node_pool = true + initial_node_count = 1 + + network = var.network_name + subnetwork = var.subnet_name + + min_master_version = var.kubernetes_version + + ip_allocation_policy { + cluster_secondary_range_name = var.pods_secondary_range_name + services_secondary_range_name = var.services_secondary_range_name + } + + workload_identity_config { + workload_pool = "${var.project_id}.svc.id.goog" + } + + logging_service = var.logging_service + monitoring_service = var.monitoring_service + + addons_config { + http_load_balancing { + disabled = false + } + horizontal_pod_autoscaling { + disabled = false + } + } + + maintenance_policy { + daily_maintenance_window { + start_time = "03:00" + } + } +} + +resource "google_container_node_pool" "main" { + for_each = var.node_pools + + name = each.key + project = var.project_id + location = var.region + cluster = google_container_cluster.main.name + + initial_node_count = each.value.initial_node_count + + autoscaling { + min_node_count = each.value.min_node_count + max_node_count = each.value.max_node_count + } + + node_config { + machine_type = each.value.machine_type + disk_size_gb = each.value.disk_size_gb + disk_type = each.value.disk_type + preemptible = each.value.preemptible + spot = each.value.spot + + oauth_scopes = [ + "https://www.googleapis.com/auth/cloud-platform" + ] + + workload_metadata_config { + mode = "GKE_METADATA" + } + + labels = merge(var.labels, each.value.labels) + + dynamic "taint" { + for_each = each.value.taints + content { + key = taint.value.key + value = taint.value.value + effect = taint.value.effect + } + } + } +} diff --git a/terraform/gcp/modules/gke/outputs.tf b/terraform/gcp/modules/gke/outputs.tf new file mode 100644 index 00000000..86e8ad88 --- /dev/null +++ b/terraform/gcp/modules/gke/outputs.tf @@ -0,0 +1,4 @@ +output "cluster_name" { value = google_container_cluster.main.name } +output "cluster_endpoint" { value = google_container_cluster.main.endpoint } +output "cluster_version" { value = google_container_cluster.main.master_version } +output "cluster_ca_certificate" { value = google_container_cluster.main.master_auth[0].cluster_ca_certificate; sensitive = true } diff --git a/terraform/gcp/modules/gke/variables.tf b/terraform/gcp/modules/gke/variables.tf new file mode 100644 index 00000000..ba9b99bf --- /dev/null +++ b/terraform/gcp/modules/gke/variables.tf @@ -0,0 +1,14 @@ +variable "project_id" { type = string } +variable "cluster_name" { type = string } +variable "region" { type = string } +variable "network_name" { type = string } +variable "subnet_name" { type = string } +variable "pods_secondary_range_name" { type = string } +variable "services_secondary_range_name" { type = string } +variable "kubernetes_version" { type = string } +variable "logging_service" { type = string } +variable "monitoring_service" { type = string } +variable "enable_workload_identity" { type = bool; default = true } +variable "enable_shielded_nodes" { type = bool; default = true } +variable "node_pools" { type = any } +variable "labels" { type = map(string); default = {} } diff --git a/terraform/gcp/modules/security/main.tf b/terraform/gcp/modules/security/main.tf new file mode 100644 index 00000000..7727a19f --- /dev/null +++ b/terraform/gcp/modules/security/main.tf @@ -0,0 +1,36 @@ +# GCP Firewall Rules Module + +resource "google_compute_firewall" "allow_internal" { + name = "${var.name_prefix}-allow-internal" + project = var.project_id + network = var.network_name + + allow { + protocol = "tcp" + ports = ["0-65535"] + } + + allow { + protocol = "udp" + ports = ["0-65535"] + } + + allow { + protocol = "icmp" + } + + source_ranges = ["10.0.0.0/8"] +} + +resource "google_compute_firewall" "allow_external" { + name = "${var.name_prefix}-allow-external" + project = var.project_id + network = var.network_name + + allow { + protocol = "tcp" + ports = ["443", "80"] + } + + source_ranges = var.allowed_cidr_blocks +} diff --git a/terraform/gcp/modules/security/outputs.tf b/terraform/gcp/modules/security/outputs.tf new file mode 100644 index 00000000..b3cde8a8 --- /dev/null +++ b/terraform/gcp/modules/security/outputs.tf @@ -0,0 +1,6 @@ +output "firewall_rules" { + value = [ + google_compute_firewall.allow_internal.name, + google_compute_firewall.allow_external.name + ] +} diff --git a/terraform/gcp/modules/security/variables.tf b/terraform/gcp/modules/security/variables.tf new file mode 100644 index 00000000..8a654198 --- /dev/null +++ b/terraform/gcp/modules/security/variables.tf @@ -0,0 +1,5 @@ +variable "project_id" { type = string } +variable "network_name" { type = string } +variable "name_prefix" { type = string } +variable "allowed_cidr_blocks" { type = list(string) } +variable "labels" { type = map(string); default = {} } diff --git a/terraform/gcp/modules/vpc/main.tf b/terraform/gcp/modules/vpc/main.tf new file mode 100644 index 00000000..f96aca09 --- /dev/null +++ b/terraform/gcp/modules/vpc/main.tf @@ -0,0 +1,67 @@ +# GCP VPC Module +# Creates VPC network with subnets and secondary ranges for GKE + +resource "google_compute_network" "main" { + name = "${var.name_prefix}-network" + project = var.project_id + auto_create_subnetworks = false + routing_mode = "REGIONAL" +} + +resource "google_compute_subnetwork" "main" { + name = "${var.name_prefix}-subnet" + project = var.project_id + region = var.region + network = google_compute_network.main.id + ip_cidr_range = var.subnet_cidr + + secondary_ip_range { + range_name = "${var.name_prefix}-pods" + ip_cidr_range = var.pods_secondary_range + } + + secondary_ip_range { + range_name = "${var.name_prefix}-services" + ip_cidr_range = var.services_secondary_range + } + + private_ip_google_access = true +} + +resource "google_compute_router" "main" { + count = var.enable_nat_gateway ? 1 : 0 + name = "${var.name_prefix}-router" + project = var.project_id + region = var.region + network = google_compute_network.main.id +} + +resource "google_compute_router_nat" "main" { + count = var.enable_nat_gateway ? 1 : 0 + name = "${var.name_prefix}-nat" + router = google_compute_router.main[0].name + region = var.region + + nat_ip_allocate_option = "AUTO_ONLY" + source_subnetwork_ip_ranges_to_nat = "ALL_SUBNETWORKS_ALL_IP_RANGES" + + log_config { + enable = true + filter = "ERRORS_ONLY" + } +} + +resource "google_compute_global_address" "private_service_connection" { + name = "${var.name_prefix}-private-service-connection" + project = var.project_id + purpose = "VPC_PEERING" + address_type = "INTERNAL" + prefix_length = 16 + network = google_compute_network.main.id +} + +resource "google_service_networking_connection" "private_vpc_connection" { + network = google_compute_network.main.id + service = "servicenetworking.googleapis.com" + reserved_peering_ranges = [google_compute_global_address.private_service_connection.name] +} diff --git a/terraform/gcp/modules/vpc/outputs.tf b/terraform/gcp/modules/vpc/outputs.tf new file mode 100644 index 00000000..0f298f06 --- /dev/null +++ b/terraform/gcp/modules/vpc/outputs.tf @@ -0,0 +1,34 @@ +output "network_name" { + description = "Name of the VPC network" + value = google_compute_network.main.name +} + +output "network_id" { + description = "ID of the VPC network" + value = google_compute_network.main.id +} + +output "subnet_name" { + description = "Name of the subnet" + value = google_compute_subnetwork.main.name +} + +output "subnet_cidr" { + description = "CIDR block of the subnet" + value = google_compute_subnetwork.main.ip_cidr_range +} + +output "pods_range_name" { + description = "Name of the pods secondary IP range" + value = "${var.name_prefix}-pods" +} + +output "services_range_name" { + description = "Name of the services secondary IP range" + value = "${var.name_prefix}-services" +} + +output "private_vpc_connection" { + description = "Private VPC connection for Cloud SQL" + value = google_service_networking_connection.private_vpc_connection.network +} diff --git a/terraform/gcp/modules/vpc/variables.tf b/terraform/gcp/modules/vpc/variables.tf new file mode 100644 index 00000000..8d627b3e --- /dev/null +++ b/terraform/gcp/modules/vpc/variables.tf @@ -0,0 +1,41 @@ +variable "project_id" { + description = "GCP project ID" + type = string +} + +variable "name_prefix" { + description = "Prefix for resource names" + type = string +} + +variable "region" { + description = "GCP region" + type = string +} + +variable "subnet_cidr" { + description = "CIDR block for subnet" + type = string +} + +variable "pods_secondary_range" { + description = "Secondary IP range for pods" + type = string +} + +variable "services_secondary_range" { + description = "Secondary IP range for services" + type = string +} + +variable "enable_nat_gateway" { + description = "Enable Cloud NAT" + type = bool + default = true +} + +variable "labels" { + description = "Labels to apply to resources" + type = map(string) + default = {} +} diff --git a/terraform/gcp/outputs.tf b/terraform/gcp/outputs.tf new file mode 100644 index 00000000..d99fd8dc --- /dev/null +++ b/terraform/gcp/outputs.tf @@ -0,0 +1,146 @@ +# ============================================================================= +# GCP DEPLOYMENT OUTPUTS +# Information customers get after successful deployment +# ============================================================================= + +# ============================================================================= +# DEPLOYMENT INFORMATION +# ============================================================================= +output "deployment_info" { + description = "Summary of deployment information" + value = { + environment_name = var.environment_name + gcp_project = var.gcp_project + gcp_region = var.gcp_region + vpc_created = var.create_vpc + gke_created = var.create_gke_cluster + database_created = var.create_database + cluster_name = var.create_gke_cluster ? module.gke[0].cluster_name : var.existing_cluster_name + } +} + +# ============================================================================= +# NETWORK INFORMATION +# ============================================================================= +output "vpc_info" { + description = "VPC network information" + value = var.create_vpc ? { + network_name = module.vpc[0].network_name + network_id = module.vpc[0].network_id + subnet_name = module.vpc[0].subnet_name + subnet_cidr = module.vpc[0].subnet_cidr + pods_range_name = module.vpc[0].pods_range_name + services_range_name = module.vpc[0].services_range_name + region = var.gcp_region + } : { + network_name = var.existing_network_name + message = "Using existing VPC" + } +} + +# ============================================================================= +# DATABASE INFORMATION +# ============================================================================= +output "database_info" { + description = "Cloud SQL connection information" + value = var.create_database ? { + instance_name = module.cloudsql[0].instance_name + connection_name = module.cloudsql[0].connection_name + private_ip_address = module.cloudsql[0].private_ip_address + database_name = module.cloudsql[0].database_name + database_version = module.cloudsql[0].database_version + tier = var.database_tier + availability_type = var.database_availability_type + } : { + instance_name = var.existing_database_instance + message = "Using existing Cloud SQL instance" + } + sensitive = true +} + +# ============================================================================= +# GKE CLUSTER INFORMATION +# ============================================================================= +output "gke_info" { + description = "GKE cluster information" + value = var.create_gke_cluster ? { + cluster_name = module.gke[0].cluster_name + cluster_endpoint = module.gke[0].cluster_endpoint + cluster_version = module.gke[0].cluster_version + node_pools = keys(var.node_pools) + kubectl_config = "gcloud container clusters get-credentials ${module.gke[0].cluster_name} --region ${var.gcp_region} --project ${var.gcp_project}" + } : { + cluster_name = var.existing_cluster_name + message = "Using existing GKE cluster" + } +} + +# ============================================================================= +# SECRETS INFORMATION +# ============================================================================= +output "secrets_info" { + description = "Information about created secrets" + value = { + secrets_created = var.create_database ? ["database-credentials"] : [] + project = var.gcp_project + } +} + +# ============================================================================= +# NEXT STEPS FOR CUSTOMER +# ============================================================================= +output "next_steps" { + description = "What to do after deployment" + value = <<-EOT + ✅ Chartsmith GCP infrastructure deployed successfully! + + 📋 What was created: + ${var.create_vpc ? "- VPC network with subnets and secondary ranges for GKE" : "- Used existing VPC"} + ${var.create_gke_cluster ? "- GKE cluster with managed node pools and Workload Identity" : "- Configured for existing GKE cluster"} + ${var.create_database ? "- Cloud SQL PostgreSQL database with pgvector extension" : "- Configured for existing database"} + - Firewall rules for GKE cluster and Cloud SQL + ${var.create_database ? "- Database credentials in Secret Manager" : ""} + + 📚 Resources: + - Project: ${var.gcp_project} + - Network: ${local.network_name} + ${var.create_gke_cluster ? "- GKE Cluster: ${module.gke[0].cluster_name}" : "- GKE Cluster: ${var.existing_cluster_name} (existing)"} + - Database: ${var.create_database ? module.cloudsql[0].connection_name : var.existing_database_instance} + ${var.create_database ? "- Database credentials: Secret Manager in ${var.gcp_project}" : ""} + + 🔧 Manual steps required: + 1. Configure kubectl access: ${var.create_gke_cluster ? "gcloud container clusters get-credentials ${module.gke[0].cluster_name} --region ${var.gcp_region} --project ${var.gcp_project}" : "gcloud container clusters get-credentials ${var.existing_cluster_name}"} + 2. Deploy Chartsmith via Helm + 3. Verify database connectivity and pgvector extension + + EOT +} + +# ============================================================================= +# TERRAFORM STATE INFORMATION +# ============================================================================= +output "terraform_info" { + description = "Terraform state information" + value = { + terraform_version = "~> 1.5" + gcp_provider_version = "~> 5.0" + project = var.gcp_project + region = var.gcp_region + state_backend = "Configure remote state backend for production use" + } +} + +# ============================================================================= +# SENSITIVE OUTPUTS (for automation) +# ============================================================================= +output "database_connection_string" { + description = "Database connection string" + value = var.create_database ? module.cloudsql[0].connection_string : "Configure manually for existing database" + sensitive = true +} + +output "database_secret_id" { + description = "Secret Manager secret ID for database credentials" + value = var.create_database ? google_secret_manager_secret.database_credentials[0].secret_id : null + sensitive = true +} diff --git a/terraform/gcp/terraform.tf b/terraform/gcp/terraform.tf new file mode 100644 index 00000000..b9789abc --- /dev/null +++ b/terraform/gcp/terraform.tf @@ -0,0 +1,28 @@ +terraform { + required_version = ">= 1.5.0" + + required_providers { + google = { + source = "hashicorp/google" + version = "~> 5.0" + } + google-beta = { + source = "hashicorp/google-beta" + version = "~> 5.0" + } + random = { + source = "hashicorp/random" + version = "~> 3.5" + } + } +} + +provider "google" { + project = var.gcp_project + region = var.gcp_region +} + +provider "google-beta" { + project = var.gcp_project + region = var.gcp_region +} diff --git a/terraform/gcp/variables.tf b/terraform/gcp/variables.tf new file mode 100644 index 00000000..26b05b6a --- /dev/null +++ b/terraform/gcp/variables.tf @@ -0,0 +1,232 @@ +# ============================================================================= +# ENVIRONMENT IDENTIFICATION +# ============================================================================= +variable "environment_name" { + description = "Environment name for this Chartsmith deployment (e.g., 'production', 'staging', 'dev')" + type = string + + validation { + condition = can(regex("^[a-z0-9-]+$", var.environment_name)) + error_message = "Environment name must contain only lowercase letters, numbers, and hyphens." + } +} + +variable "gcp_project" { + description = "GCP project ID for deployment" + type = string +} + +variable "gcp_region" { + description = "GCP region for deployment" + type = string + default = "us-central1" +} + +# ============================================================================= +# INFRASTRUCTURE CHOICES +# ============================================================================= +variable "create_vpc" { + description = "Create a new VPC for Chartsmith" + type = bool + default = true +} + +variable "create_gke_cluster" { + description = "Create a new GKE cluster for Chartsmith" + type = bool + default = true +} + +variable "create_database" { + description = "Create a new Cloud SQL PostgreSQL database for Chartsmith" + type = bool + default = true +} + +# ============================================================================= +# EXISTING INFRASTRUCTURE +# ============================================================================= +variable "existing_network_name" { + description = "Name of existing VPC network (required if create_vpc = false)" + type = string + default = "" +} + +variable "existing_network_id" { + description = "ID of existing VPC network (required if create_vpc = false)" + type = string + default = "" +} + +variable "existing_subnet_name" { + description = "Name of existing subnet (required if create_vpc = false)" + type = string + default = "" +} + +variable "existing_pods_range_name" { + description = "Name of existing pods secondary IP range (required if create_vpc = false)" + type = string + default = "" +} + +variable "existing_services_range_name" { + description = "Name of existing services secondary IP range (required if create_vpc = false)" + type = string + default = "" +} + +variable "existing_cluster_name" { + description = "Name of existing GKE cluster (required if create_gke_cluster = false)" + type = string + default = "" +} + +variable "existing_database_instance" { + description = "Name of existing Cloud SQL instance (required if create_database = false)" + type = string + default = "" +} + +# ============================================================================= +# VPC CONFIGURATION +# ============================================================================= +variable "subnet_cidr" { + description = "CIDR block for subnet" + type = string + default = "10.0.0.0/24" +} + +variable "pods_secondary_range" { + description = "Secondary IP range for pods" + type = string + default = "10.4.0.0/14" +} + +variable "services_secondary_range" { + description = "Secondary IP range for services" + type = string + default = "10.8.0.0/20" +} + +# ============================================================================= +# GKE CONFIGURATION +# ============================================================================= +variable "kubernetes_version" { + description = "Kubernetes version for GKE cluster" + type = string + default = "1.28" +} + +variable "node_pools" { + description = "GKE node pool configurations" + type = map(object({ + machine_type = string + min_node_count = number + max_node_count = number + initial_node_count = number + disk_size_gb = number + disk_type = optional(string, "pd-standard") + preemptible = optional(bool, false) + spot = optional(bool, false) + labels = optional(map(string), {}) + taints = optional(list(object({ + key = string + value = string + effect = string + })), []) + })) + default = { + main = { + machine_type = "n1-standard-2" + min_node_count = 2 + max_node_count = 10 + initial_node_count = 3 + disk_size_gb = 50 + } + } +} + +# ============================================================================= +# DATABASE CONFIGURATION +# ============================================================================= +variable "database_tier" { + description = "Cloud SQL machine tier" + type = string + default = "db-custom-2-7680" +} + +variable "database_version" { + description = "PostgreSQL version" + type = string + default = "POSTGRES_15" +} + +variable "database_availability_type" { + description = "Availability type (ZONAL or REGIONAL)" + type = string + default = "ZONAL" +} + +variable "database_disk_size" { + description = "Disk size for Cloud SQL instance (GB)" + type = number + default = 100 +} + +variable "database_disk_autoresize_limit" { + description = "Maximum disk size for autoresize (GB)" + type = number + default = 500 +} + +variable "database_name" { + description = "Name of the database to create" + type = string + default = "chartsmith" +} + +variable "database_username" { + description = "Username for database" + type = string + default = "chartsmith" +} + +# ============================================================================= +# SECURITY +# ============================================================================= +variable "allowed_cidr_blocks" { + description = "CIDR blocks allowed to access Chartsmith" + type = list(string) + default = ["0.0.0.0/0"] +} + +# ============================================================================= +# BACKUP CONFIGURATION +# ============================================================================= +variable "backup_start_time" { + description = "Preferred backup start time (HH:MM)" + type = string + default = "03:00" +} + +# ============================================================================= +# OPTIONAL FEATURES +# ============================================================================= +variable "enable_nat_gateway" { + description = "Enable Cloud NAT for private nodes" + type = bool + default = true +} + +# ============================================================================= +# LABELS +# ============================================================================= +variable "labels" { + description = "Additional labels to apply to all resources" + type = map(string) + default = { + application = "chartsmith" + managed_by = "terraform" + } +}