├── cmd └── eks-node-viewer │ ├── .gitignore │ ├── main.go │ └── flag.go ├── .gitignore ├── NOTICE ├── .static └── screenshot.png ├── hack ├── gen_licenses.sh ├── attribution.tmpl ├── homebrew.go └── boilerplate.go ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── .github ├── dependabot.yaml └── workflows │ ├── test.yaml │ └── release.yaml ├── pkg ├── pricing │ └── pricing.go ├── model │ ├── stats.go │ ├── style.go │ ├── pod.go │ ├── pod_test.go │ ├── cluster.go │ ├── node_test.go │ ├── cluster_test.go │ ├── node.go │ └── uimodel.go ├── client │ ├── client.go │ └── controller.go ├── text │ └── colortabwriter.go └── aws │ ├── zz_generated_aws_cn.pricing.go │ ├── pricing.go │ ├── zz_generated_aws.pricing.go │ └── zz_generated_aws_us_gov.pricing.go ├── .goreleaser.yaml ├── .golangci.yaml ├── Makefile ├── CONTRIBUTING.md ├── README.md ├── go.mod ├── LICENSE └── go.sum /cmd/eks-node-viewer/.gitignore: -------------------------------------------------------------------------------- 1 | eks-node-viewer 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | /eks-node-viewer 3 | coverage.out 4 | /dist 5 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | -------------------------------------------------------------------------------- /.static/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awslabs/eks-node-viewer/HEAD/.static/screenshot.png -------------------------------------------------------------------------------- /hack/gen_licenses.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | go install github.com/google/go-licenses@latest 3 | go mod download 4 | GOROOT=$(go env GOROOT) go-licenses report ./... --template hack/attribution.tmpl > ATTRIBUTION.md 5 | 6 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Require approvals from someone in the owner team before merging 2 | # More information here: https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners 3 | 4 | * @awslabs/karpenter 5 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /hack/attribution.tmpl: -------------------------------------------------------------------------------- 1 | # Open Source Software Attribution 2 | 3 | 4 | The Amazon eks-node-viewer Product includes the following third-party software/licensing: 5 | 6 | 7 | {{ range . }} 8 | ## {{ .Name }} 9 | 10 | * Name: {{ .Name }} 11 | * Version: {{ .Version }} 12 | * License: [{{ .LicenseName }}]({{ .LicenseURL }}) 13 | 14 | ``` 15 | {{ .LicenseText }} 16 | ``` 17 | {{ end }} 18 | 19 | -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://json.schemastore.org/dependabot-2.0.json 2 | version: 2 3 | updates: 4 | - package-ecosystem: "gomod" 5 | directory: "/" 6 | groups: 7 | k8s-dependencies: 8 | patterns: 9 | - "k8s.io*" 10 | - "sigs.k8s.io/*" 11 | aws-dependencies: 12 | patterns: 13 | - "github.com/aws*" 14 | schedule: 15 | interval: "weekly" -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | # This workflow will build a golang project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go 3 | 4 | name: Go code tests 5 | 6 | on: 7 | push: 8 | branches: [ "main" ] 9 | pull_request: 10 | branches: [ "main" ] 11 | workflow_dispatch: {} 12 | 13 | permissions: 14 | contents: read 15 | 16 | jobs: 17 | 18 | build: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v4.1.5 22 | 23 | - name: Set up Go 24 | uses: actions/setup-go@v5.0.1 25 | with: 26 | go-version: 'stable' 27 | check-latest: true 28 | 29 | - name: Build 30 | run: make build 31 | 32 | - name: Test 33 | run: make test 34 | -------------------------------------------------------------------------------- /pkg/pricing/pricing.go: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | 15 | package pricing 16 | 17 | import "github.com/awslabs/eks-node-viewer/pkg/model" 18 | 19 | // Provider provides node prices for display in the node viewer 20 | type Provider interface { 21 | NodePrice(n *model.Node) (float64, bool) 22 | OnUpdate(onUpdate func()) 23 | } 24 | -------------------------------------------------------------------------------- /pkg/model/stats.go: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | 15 | package model 16 | 17 | import v1 "k8s.io/api/core/v1" 18 | 19 | type Stats struct { 20 | NumNodes int 21 | AllocatableResources v1.ResourceList 22 | UsedResources v1.ResourceList 23 | PercentUsedResoruces map[v1.ResourceName]float64 24 | Nodes []*Node 25 | TotalPods int 26 | PodsByPhase map[v1.PodPhase]int 27 | BoundPodCount int 28 | TotalPrice float64 29 | } 30 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | project_name: eks-node-viewer 2 | version: 2 3 | 4 | source: 5 | enabled: false 6 | 7 | before: 8 | hooks: 9 | - go mod download 10 | 11 | builds: 12 | - binary: eks-node-viewer 13 | main: ./cmd/eks-node-viewer 14 | targets: 15 | - linux_amd64 16 | - linux_arm64 17 | - windows_amd64 18 | - darwin_amd64 19 | - darwin_arm64 20 | env: 21 | - CGO_ENABLED=0 22 | flags: 23 | - -v 24 | 25 | universal_binaries: 26 | - replace: true 27 | 28 | archives: 29 | - id: eks-node-viewer 30 | name_template: >- 31 | {{ .ProjectName }}_ 32 | {{- title .Os }}_ 33 | {{- if eq .Arch "amd64" }}x86_64 34 | {{- else if eq .Arch "386" }}i386 35 | {{- else }}{{ .Arch }}{{ end }} 36 | format: binary 37 | 38 | release: 39 | prerelease: auto 40 | 41 | snapshot: 42 | version_template: "{{ .Tag }}-next" 43 | 44 | checksum: 45 | name_template: "{{ .ProjectName }}_{{ .Version }}_sha256_checksums.txt" 46 | algorithm: sha256 47 | 48 | changelog: 49 | sort: asc 50 | use: github 51 | filters: 52 | exclude: 53 | - '^docs:' 54 | - '^test:' 55 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | # This workflow will build and release golang project 2 | 3 | name: Binary release 4 | 5 | on: 6 | push: 7 | tags: 8 | - '*' 9 | 10 | permissions: 11 | contents: write 12 | 13 | jobs: 14 | build: 15 | name: Build 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v4.1.5 20 | with: 21 | fetch-depth: 0 # disable shallow clone - get all 22 | 23 | - name: Set up Go 1.x 24 | uses: actions/setup-go@v5.0.1 25 | with: 26 | go-version: stable 27 | id: go 28 | 29 | - name: Fetch build tag 30 | run: | 31 | VERSION=${GITHUB_REF#refs/tags/} 32 | echo "VERSION=$VERSION" >> $GITHUB_ENV 33 | shell: /bin/bash -e {0} 34 | 35 | - name: Print build tag 36 | run: echo "Building release ${VERSION}" 37 | 38 | 39 | - name: Generate code 40 | run: make generate 41 | 42 | - name: Check for changes 43 | run: | 44 | git diff 45 | git diff-index --quiet HEAD 46 | 47 | - name: Run GoReleaser 48 | uses: goreleaser/goreleaser-action@v4 49 | with: 50 | distribution: goreleaser 51 | version: latest 52 | args: release --clean 53 | workdir: . 54 | env: 55 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 56 | -------------------------------------------------------------------------------- /pkg/model/style.go: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | package model 15 | 16 | import ( 17 | "fmt" 18 | "strings" 19 | 20 | "github.com/charmbracelet/bubbles/progress" 21 | "github.com/charmbracelet/lipgloss" 22 | ) 23 | 24 | type Style struct { 25 | green func(strs ...string) string 26 | yellow func(strs ...string) string 27 | red func(strs ...string) string 28 | gradient progress.Option 29 | } 30 | 31 | func ParseStyle(style string) (*Style, error) { 32 | colors := strings.Split(style, ",") 33 | if len(colors) != 3 { 34 | return nil, fmt.Errorf("three colors must be provided for the style, found %d (%q)", len(colors), style) 35 | } 36 | s := &Style{} 37 | s.green = lipgloss.NewStyle().Foreground(lipgloss.Color(colors[0])).Render 38 | s.yellow = lipgloss.NewStyle().Foreground(lipgloss.Color(colors[1])).Render 39 | s.red = lipgloss.NewStyle().Foreground(lipgloss.Color(colors[2])).Render 40 | 41 | s.gradient = progress.WithGradient(colors[2], colors[0]) 42 | return s, nil 43 | } 44 | -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- 1 | # See https://github.com/golangci/golangci-lint/blob/master/.golangci.example.yml 2 | run: 3 | tests: true 4 | 5 | timeout: 5m 6 | 7 | skip-dirs: 8 | - tools 9 | - website 10 | - hack 11 | - charts 12 | - designs 13 | 14 | linters: 15 | enable: 16 | - asciicheck 17 | - bidichk 18 | - errorlint 19 | - exportloopref 20 | - gosec 21 | - revive 22 | - stylecheck 23 | - tparallel 24 | - unused 25 | - unconvert 26 | - unparam 27 | - gocyclo 28 | - govet 29 | - goimports 30 | - goheader 31 | - misspell 32 | - nilerr 33 | disable: 34 | - prealloc 35 | 36 | linters-settings: 37 | gocyclo: 38 | min-complexity: 11 39 | govet: 40 | check-shadowing: true 41 | misspell: 42 | locale: US 43 | ignore-words: [] 44 | goimports: 45 | local-prefixes: github.com/awslabs/eks-node-viewer 46 | goheader: 47 | template: |- 48 | Licensed under the Apache License, Version 2.0 (the "License"); 49 | you may not use this file except in compliance with the License. 50 | You may obtain a copy of the License at 51 | 52 | http://www.apache.org/licenses/LICENSE-2.0 53 | 54 | Unless required by applicable law or agreed to in writing, software 55 | distributed under the License is distributed on an "AS IS" BASIS, 56 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 57 | See the License for the specific language governing permissions and 58 | limitations under the License. 59 | issues: 60 | fix: true 61 | exclude: ['declaration of "(err|ctx)" shadows declaration at'] 62 | exclude-rules: 63 | - linters: 64 | - goheader 65 | path: 'zz_(.+)\.go' 66 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: help clean verify boilerplate licenses download coverage generate test 2 | 3 | NO_COLOR=\033[0m 4 | GREEN=\033[32;01m 5 | YELLOW=\033[33;01m 6 | RED=\033[31;01m 7 | TEST_PKGS=./pkg/... ./cmd/... 8 | 9 | help: ## Show this help 10 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[33m%-20s\033[0m %s\n", $$1, $$2}' 11 | 12 | build: generate ## Build 13 | go build -ldflags="-s -w -X main.version=local -X main.builtBy=Makefile" ./cmd/eks-node-viewer 14 | 15 | goreleaser: ## Release snapshot 16 | goreleaser build --snapshot --clean 17 | 18 | download: ## Download dependencies 19 | go mod download 20 | go mod tidy 21 | 22 | licenses: download ## Check licenses 23 | go-licenses check ./... --allowed_licenses=MIT,Apache-2.0,BSD-3-Clause,ISC \ 24 | --ignore github.com/mattn/go-localereader # MIT 25 | 26 | boilerplate: ## Add license headers 27 | go run hack/boilerplate.go ./ 28 | 29 | verify: boilerplate licenses download ## Format and Lint 30 | gofmt -w -s ./. 31 | golangci-lint run 32 | 33 | coverage: ## Run tests w/ coverage 34 | go test -coverprofile=coverage.out $(TEST_PKGS) 35 | go tool cover -html=coverage.out 36 | 37 | generate: ## Generate attribution 38 | # run generate twice, gen_licenses needs the ATTRIBUTION file or it fails. The second run 39 | # ensures that the latest copy is embedded when we build. 40 | go generate ./... 41 | ./hack/gen_licenses.sh 42 | go generate ./... 43 | curl https://raw.githubusercontent.com/aws/karpenter-provider-aws/main/pkg/providers/pricing/zz_generated.pricing_aws.go > ./pkg/aws/zz_generated_aws.pricing.go 44 | curl https://raw.githubusercontent.com/aws/karpenter-provider-aws/main/pkg/providers/pricing/zz_generated.pricing_aws_cn.go > ./pkg/aws/zz_generated_aws_cn.pricing.go 45 | curl https://raw.githubusercontent.com/aws/karpenter-provider-aws/main/pkg/providers/pricing/zz_generated.pricing_aws_us_gov.go > ./pkg/aws/zz_generated_aws_us_gov.pricing.go 46 | sed -i'.bkup' 's/package pricing/package aws/' pkg/aws/zz_generated* 47 | rm -f pkg/aws/*.bkup 48 | 49 | clean: ## Clean artifacts 50 | rm -rf eks-node-viewer 51 | rm -rf dist/ 52 | 53 | test: 54 | go test -v $(TEST_PKGS) 55 | 56 | -------------------------------------------------------------------------------- /pkg/client/client.go: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | 15 | package client 16 | 17 | import ( 18 | "strings" 19 | 20 | "k8s.io/apimachinery/pkg/runtime/schema" 21 | "k8s.io/client-go/kubernetes" 22 | "k8s.io/client-go/kubernetes/scheme" 23 | _ "k8s.io/client-go/plugin/pkg/client/auth" // pull auth 24 | "k8s.io/client-go/rest" 25 | "k8s.io/client-go/tools/clientcmd" 26 | 27 | karpv1apis "sigs.k8s.io/karpenter/pkg/apis" 28 | karpv1 "sigs.k8s.io/karpenter/pkg/apis/v1" 29 | ) 30 | 31 | func NewKubernetes(kubeconfig, context string) (*kubernetes.Clientset, error) { 32 | config, err := getConfig(kubeconfig, context) 33 | if err != nil { 34 | return nil, err 35 | } 36 | clientset, err := kubernetes.NewForConfig(config) 37 | if err != nil { 38 | return nil, err 39 | } 40 | return clientset, err 41 | } 42 | 43 | func NewNodeClaims(kubeconfig, context string) (*rest.RESTClient, error) { 44 | c, err := getConfig(kubeconfig, context) 45 | if err != nil { 46 | return nil, err 47 | } 48 | 49 | gv := schema.GroupVersion{Group: karpv1apis.Group, Version: "v1"} 50 | scheme.Scheme.AddKnownTypes(gv, 51 | &karpv1.NodeClaim{}, 52 | &karpv1.NodeClaimList{}) 53 | 54 | config := *c 55 | config.ContentConfig.GroupVersion = &gv 56 | config.APIPath = "/apis" 57 | config.NegotiatedSerializer = scheme.Codecs.WithoutConversion() 58 | config.UserAgent = rest.DefaultKubernetesUserAgent() 59 | 60 | return rest.RESTClientFor(&config) 61 | } 62 | 63 | func getConfig(kubeconfig, context string) (*rest.Config, error) { 64 | // use the current context in kubeconfig 65 | return clientcmd.NewNonInteractiveDeferredLoadingClientConfig( 66 | &clientcmd.ClientConfigLoadingRules{Precedence: strings.Split(kubeconfig, ":")}, 67 | &clientcmd.ConfigOverrides{CurrentContext: context}).ClientConfig() 68 | } 69 | -------------------------------------------------------------------------------- /hack/homebrew.go: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | package main 15 | 16 | import ( 17 | "bufio" 18 | "flag" 19 | "fmt" 20 | "log" 21 | "net/http" 22 | "os" 23 | "strings" 24 | "text/template" 25 | ) 26 | 27 | type Data struct { 28 | Version string 29 | DarwinAll string 30 | LinuxArm string 31 | LinuxX64 string 32 | WindowsX64 string 33 | } 34 | 35 | // Example Usage: 36 | // go run hack/homebrew.go --version 0.6.0 > ../aws-homebrew-tap/bottle-configs/eks-node-viewer.json 37 | 38 | func main() { 39 | var data Data 40 | flag.StringVar(&data.Version, "version", "", "version to generate a homebrew config for") 41 | flag.Parse() 42 | if data.Version == "" { 43 | log.Fatalf("version must be supplied") 44 | } 45 | 46 | bconfig, err := template.New("bottle-config").Parse(`{ 47 | "name": "eks-node-viewer", 48 | "version": "{{.Version}}", 49 | "bin": "eks-node-viewer", 50 | "bottle": { 51 | "root_url": "https://github.com/awslabs/eks-node-viewer/releases/download/v{{.Version}}/eks-node-viewer", 52 | "sha256": { 53 | "sierra": "{{.DarwinAll}}", 54 | "linux": "{{.LinuxX64}}", 55 | "linux_arm": "{{.LinuxArm}}" 56 | } 57 | } 58 | } 59 | `) 60 | if err != nil { 61 | log.Fatalf("unable to parse template, %s", err) 62 | } 63 | 64 | // fetch and parse the checksums 65 | req, err := http.Get(fmt.Sprintf(`https://github.com/awslabs/eks-node-viewer/releases/download/v%s/eks-node-viewer_%s_sha256_checksums.txt`, data.Version, data.Version)) 66 | if err != nil { 67 | log.Fatalf("fetching checksums, %s", err) 68 | } 69 | defer req.Body.Close() 70 | sc := bufio.NewScanner(req.Body) 71 | for sc.Scan() { 72 | fields := strings.Fields(sc.Text()) 73 | if len(fields) != 2 { 74 | log.Fatalf("unavble to parse line, %q", sc.Text()) 75 | } 76 | hash := fields[0] 77 | bin := fields[1] 78 | switch bin { 79 | case "eks-node-viewer_Darwin_all": 80 | data.DarwinAll = hash 81 | case "eks-node-viewer_Linux_arm64": 82 | data.LinuxArm = hash 83 | case "eks-node-viewer_Linux_x86_64": 84 | data.LinuxX64 = hash 85 | case "eks-node-viewer_Windows_x86_64.exe": 86 | data.WindowsX64 = hash 87 | default: 88 | log.Fatalf("unsupported bin, %s", bin) 89 | } 90 | } 91 | 92 | if err := bconfig.Execute(os.Stdout, data); err != nil { 93 | log.Fatalf("executing template, %s", err) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /hack/boilerplate.go: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | 15 | package main 16 | 17 | import ( 18 | "bytes" 19 | "fmt" 20 | "io" 21 | "io/fs" 22 | "log" 23 | "os" 24 | "path/filepath" 25 | "strings" 26 | ) 27 | 28 | const apacheLicense = `/* 29 | Licensed under the Apache License, Version 2.0 (the "License"); 30 | you may not use this file except in compliance with the License. 31 | You may obtain a copy of the License at 32 | 33 | http://www.apache.org/licenses/LICENSE-2.0 34 | 35 | Unless required by applicable law or agreed to in writing, software 36 | distributed under the License is distributed on an "AS IS" BASIS, 37 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 38 | See the License for the specific language governing permissions and 39 | limitations under the License. 40 | */ 41 | ` 42 | 43 | func main() { 44 | for _, path := range os.Args[1:] { 45 | if err := filepath.WalkDir(path, addLicense); err != nil { 46 | log.Printf("processing %s, %s", path, err) 47 | } 48 | } 49 | } 50 | 51 | func addLicense(path string, _ fs.DirEntry, err error) error { 52 | if !strings.HasSuffix(path, ".go") { 53 | return nil 54 | } 55 | 56 | srcFile, err := os.Open(path) 57 | if err != nil { 58 | return fmt.Errorf("opening %s, %w", srcFile.Name(), err) 59 | } 60 | defer srcFile.Close() 61 | buf := make([]byte, len(apacheLicense)+50) 62 | _, err = srcFile.Read(buf) 63 | if err != nil { 64 | return fmt.Errorf("reading %s, %w", srcFile.Name(), err) 65 | } 66 | if bytes.Index(buf, []byte(`http://www.apache.org/licenses/LICENSE-2.0`)) != -1 { 67 | return nil 68 | } 69 | log.Println("adding license to", path) 70 | 71 | tmp, err := os.CreateTemp("", "") 72 | if err != nil { 73 | return fmt.Errorf("creating temp file, %w", err) 74 | } 75 | defer os.Remove(tmp.Name()) 76 | defer tmp.Close() 77 | 78 | // write the license to the file 79 | fmt.Fprint(tmp, apacheLicense) 80 | if _, err := srcFile.Seek(0, io.SeekStart); err != nil { 81 | return fmt.Errorf("seeking, %w", err) 82 | } 83 | 84 | // followed by the source file contents 85 | _, err = io.Copy(tmp, srcFile) 86 | if err != nil { 87 | return fmt.Errorf("creating source, %w", err) 88 | } 89 | 90 | if err := os.Remove(path); err != nil { 91 | return fmt.Errorf("removing %s, %w", path, err) 92 | } 93 | if err := os.Rename(tmp.Name(), path); err != nil { 94 | return fmt.Errorf("moving %s => %s, %w", tmp.Name(), path, err) 95 | } 96 | return nil 97 | } 98 | -------------------------------------------------------------------------------- /cmd/eks-node-viewer/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | package main 15 | 16 | import ( 17 | "context" 18 | _ "embed" 19 | "errors" 20 | "flag" 21 | "fmt" 22 | "log" 23 | "os" 24 | "strings" 25 | 26 | "github.com/aws/aws-sdk-go-v2/config" 27 | tea "github.com/charmbracelet/bubbletea" 28 | "k8s.io/apimachinery/pkg/labels" 29 | 30 | "github.com/awslabs/eks-node-viewer/pkg/aws" 31 | "github.com/awslabs/eks-node-viewer/pkg/client" 32 | "github.com/awslabs/eks-node-viewer/pkg/model" 33 | ) 34 | 35 | //go:generate cp -r ../../ATTRIBUTION.md ./ 36 | //go:embed ATTRIBUTION.md 37 | var attribution string 38 | 39 | func main() { 40 | flags, err := ParseFlags() 41 | if err != nil { 42 | if errors.Is(err, flag.ErrHelp) { 43 | os.Exit(0) 44 | } 45 | log.Fatalf("cannot parse flags: %v", err) 46 | } 47 | 48 | if flags.ShowAttribution { 49 | fmt.Println(attribution) 50 | os.Exit(0) 51 | } 52 | 53 | if flags.Version { 54 | fmt.Printf("eks-node-viewer version %s\n", version) 55 | fmt.Printf("commit: %s\n", commit) 56 | fmt.Printf("built at: %s\n", date) 57 | fmt.Printf("built by: %s\n", builtBy) 58 | os.Exit(0) 59 | } 60 | 61 | cs, err := client.NewKubernetes(flags.Kubeconfig, flags.Context) 62 | if err != nil { 63 | log.Fatalf("creating client, %s", err) 64 | } 65 | nodeClaimClient, err := client.NewNodeClaims(flags.Kubeconfig, flags.Context) 66 | if err != nil { 67 | log.Fatalf("creating node claim client, %s", err) 68 | } 69 | ctx, cancel := context.WithCancel(context.Background()) 70 | 71 | pprov := aws.NewStaticPricingProvider() 72 | style, err := model.ParseStyle(flags.Style) 73 | if err != nil { 74 | log.Fatalf("creating style, %s", err) 75 | } 76 | m := model.NewUIModel(strings.Split(flags.ExtraLabels, ","), flags.NodeSort, style) 77 | m.DisablePricing = flags.DisablePricing 78 | m.SetResources(strings.FieldsFunc(flags.Resources, func(r rune) bool { return r == ',' })) 79 | 80 | var nodeSelector labels.Selector 81 | if ns, err := labels.Parse(flags.NodeSelector); err != nil { 82 | log.Fatalf("parsing node selector: %s", err) 83 | } else { 84 | nodeSelector = ns 85 | } 86 | 87 | if !flags.DisablePricing { 88 | // Use AWS SDK Go v2 for configuration 89 | cfg, err := config.LoadDefaultConfig(ctx, config.WithSharedConfigProfile("")) 90 | if err != nil { 91 | log.Fatalf("unable to load AWS SDK config: %s", err) 92 | } 93 | pprov = aws.NewPricingProvider(ctx, cfg) 94 | } 95 | controller := client.NewController(cs, nodeClaimClient, m, nodeSelector, pprov) 96 | 97 | controller.Start(ctx) 98 | 99 | if _, err := tea.NewProgram(m, tea.WithAltScreen()).Run(); err != nil { 100 | log.Fatalf("error running tea: %s", err) 101 | } 102 | cancel() 103 | } 104 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *main* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | -------------------------------------------------------------------------------- /pkg/text/colortabwriter.go: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | 15 | package text 16 | 17 | import ( 18 | "io" 19 | ) 20 | 21 | const debug = false 22 | 23 | type ColorTabWriter struct { 24 | output io.Writer 25 | padding int 26 | minWidth int 27 | tabWidth int 28 | 29 | contents [][][]byte 30 | cellWidths []int 31 | } 32 | 33 | func NewColorTabWriter(output io.Writer, minWidth, tabWidth, padding int) *ColorTabWriter { 34 | return &ColorTabWriter{ 35 | output: output, 36 | minWidth: minWidth, 37 | tabWidth: tabWidth, 38 | padding: padding, 39 | } 40 | } 41 | 42 | func (c *ColorTabWriter) Write(buf []byte) (n int, err error) { 43 | for _, ch := range buf { 44 | switch ch { 45 | case '\t': 46 | c.newCell() 47 | case '\n': 48 | c.newLine() 49 | default: 50 | c.append(ch) 51 | } 52 | } 53 | return len(buf), nil 54 | } 55 | 56 | func (c *ColorTabWriter) Flush() { 57 | maxWidth := 0 58 | for _, line := range c.contents { 59 | // ensure we track a cell width for every cell 60 | for len(line) > len(c.cellWidths) { 61 | c.cellWidths = append(c.cellWidths, 0) 62 | } 63 | for i, cell := range line { 64 | cellLen := strlen(cell) 65 | if cellLen > c.cellWidths[i] { 66 | c.cellWidths[i] = cellLen 67 | } 68 | if cellLen > maxWidth { 69 | maxWidth = cellLen 70 | } 71 | } 72 | } 73 | 74 | padding := make([]byte, maxWidth+2) 75 | for i := range padding { 76 | if debug { 77 | padding[i] = '.' 78 | } else { 79 | padding[i] = ' ' 80 | } 81 | } 82 | 83 | for _, line := range c.contents { 84 | if len(line) == 0 { 85 | continue 86 | } 87 | for i, cell := range line { 88 | // collapse empty columns 89 | if c.cellWidths[i] == 0 { 90 | continue 91 | } 92 | cellStrLen := strlen(cell) 93 | cellPadding := c.cellWidths[i] + c.padding - cellStrLen 94 | 95 | if debug { 96 | c.output.Write([]byte("|")) 97 | } 98 | c.output.Write(cell) 99 | c.output.Write(padding[0:cellPadding]) 100 | } 101 | c.output.Write([]byte("\n")) 102 | } 103 | c.contents = nil 104 | c.cellWidths = nil 105 | } 106 | 107 | func strlen(cell []byte) int { 108 | nChars := 0 109 | inEscape := false 110 | for _, c := range cell { 111 | switch c { 112 | case 0x1b: 113 | inEscape = true 114 | case 'm': 115 | if inEscape { 116 | inEscape = false 117 | } else { 118 | nChars++ 119 | } 120 | default: 121 | if !inEscape { 122 | nChars++ 123 | } 124 | } 125 | } 126 | return nChars 127 | } 128 | 129 | func (c *ColorTabWriter) newCell() { 130 | if len(c.contents) == 0 { 131 | c.newLine() 132 | } 133 | c.contents[len(c.contents)-1] = append(c.contents[len(c.contents)-1], []byte{}) 134 | } 135 | 136 | func (c *ColorTabWriter) newLine() { 137 | c.contents = append(c.contents, [][]byte{}) 138 | } 139 | 140 | func (c *ColorTabWriter) append(ch byte) { 141 | if len(c.contents) == 0 { 142 | c.newLine() 143 | } 144 | lastLine := len(c.contents) - 1 145 | if len(c.contents[lastLine]) == 0 { 146 | c.newCell() 147 | } 148 | lastCell := len(c.contents[lastLine]) - 1 149 | c.contents[lastLine][lastCell] = append(c.contents[lastLine][lastCell], ch) 150 | } 151 | -------------------------------------------------------------------------------- /pkg/model/pod.go: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | 15 | package model 16 | 17 | import ( 18 | "log" 19 | "regexp" 20 | "strconv" 21 | "sync" 22 | 23 | v1 "k8s.io/api/core/v1" 24 | "k8s.io/apimachinery/pkg/api/resource" 25 | ) 26 | 27 | // Pod is our pod model used for internal storage and display 28 | type Pod struct { 29 | mu sync.RWMutex 30 | pod v1.Pod 31 | } 32 | 33 | // NewPod constructs a pod model based off of the K8s pod object 34 | func NewPod(n *v1.Pod) *Pod { 35 | return &Pod{ 36 | pod: *n, 37 | } 38 | } 39 | 40 | // Update updates the pod model, replacing it with a shallow copy of the provided pod 41 | func (p *Pod) Update(pod *v1.Pod) { 42 | p.mu.Lock() 43 | defer p.mu.Unlock() 44 | p.pod = *pod 45 | } 46 | 47 | // IsScheduled returns true if the pod has been scheduled to a node 48 | func (p *Pod) IsScheduled() bool { 49 | p.mu.RLock() 50 | defer p.mu.RUnlock() 51 | return p.pod.Spec.NodeName != "" 52 | } 53 | 54 | // NodeName returns the node that the pod is scheduled against, or an empty string 55 | func (p *Pod) NodeName() string { 56 | p.mu.RLock() 57 | defer p.mu.RUnlock() 58 | return p.pod.Spec.NodeName 59 | } 60 | 61 | // Namespace returns the namespace of the pod 62 | func (p *Pod) Namespace() string { 63 | p.mu.RLock() 64 | defer p.mu.RUnlock() 65 | return p.pod.Namespace 66 | } 67 | 68 | // Name returns the name of the pod 69 | func (p *Pod) Name() string { 70 | p.mu.RLock() 71 | defer p.mu.RUnlock() 72 | return p.pod.Name 73 | } 74 | 75 | // Phase returns the pod phase 76 | func (p *Pod) Phase() v1.PodPhase { 77 | p.mu.RLock() 78 | defer p.mu.RUnlock() 79 | return p.pod.Status.Phase 80 | } 81 | 82 | // Requested returns the sum of the resources requested by the pod. 83 | // Also include resources for init containers that are sidecars as described in 84 | // https://kubernetes.io/blog/2023/08/25/native-sidecar-containers . 85 | func (p *Pod) Requested() v1.ResourceList { 86 | p.mu.RLock() 87 | defer p.mu.RUnlock() 88 | requested := v1.ResourceList{} 89 | for _, c := range p.pod.Spec.InitContainers { 90 | if c.RestartPolicy == nil || *c.RestartPolicy != v1.ContainerRestartPolicyAlways { 91 | continue 92 | } 93 | for rn, q := range c.Resources.Requests { 94 | existing := requested[rn] 95 | existing.Add(q) 96 | requested[rn] = existing 97 | } 98 | } 99 | for _, c := range p.pod.Spec.Containers { 100 | for rn, q := range c.Resources.Requests { 101 | existing := requested[rn] 102 | existing.Add(q) 103 | requested[rn] = existing 104 | } 105 | } 106 | requested[v1.ResourcePods] = resource.MustParse("1") 107 | return requested 108 | } 109 | 110 | var fargateCapacityRe = regexp.MustCompile("(.*?)vCPU (.*?)GB") 111 | 112 | func (p *Pod) FargateCapacityProvisioned() (float64, float64, bool) { 113 | provisioned, ok := p.pod.Annotations["CapacityProvisioned"] 114 | if !ok { 115 | return 0, 0, false 116 | } 117 | 118 | match := fargateCapacityRe.FindStringSubmatch(provisioned) 119 | if len(match) != 3 { 120 | log.Printf("unable to parse %q for fargate provisioner capacity", provisioned) 121 | } 122 | cpu, err := strconv.ParseFloat(match[1], 64) 123 | if err != nil { 124 | log.Printf("unable to parse CPU from fargate capacity, %q, %s", provisioned, err) 125 | return 0, 0, false 126 | } 127 | mem, err := strconv.ParseFloat(match[2], 64) 128 | if err != nil { 129 | log.Printf("unable to parse memory from fargate capacity, %q, %s", provisioned, err) 130 | return 0, 0, false 131 | } 132 | return cpu, mem, true 133 | } 134 | -------------------------------------------------------------------------------- /pkg/model/pod_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | 15 | package model_test 16 | 17 | import ( 18 | "testing" 19 | 20 | v1 "k8s.io/api/core/v1" 21 | "k8s.io/apimachinery/pkg/api/resource" 22 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 23 | 24 | "github.com/awslabs/eks-node-viewer/pkg/model" 25 | ) 26 | 27 | func testPod(namespace, name string) *v1.Pod { 28 | restartAlways := v1.ContainerRestartPolicyAlways 29 | p := &v1.Pod{ 30 | ObjectMeta: metav1.ObjectMeta{ 31 | Namespace: namespace, 32 | Name: name, 33 | }, 34 | Status: v1.PodStatus{ 35 | Phase: v1.PodPending, 36 | }, 37 | Spec: v1.PodSpec{ 38 | InitContainers: []v1.Container{ 39 | { 40 | Image: "normalinit", 41 | Name: "container", 42 | Resources: v1.ResourceRequirements{ 43 | Requests: v1.ResourceList{ 44 | v1.ResourceCPU: resource.MustParse("1"), 45 | v1.ResourceMemory: resource.MustParse("1Gi"), 46 | }, 47 | }, 48 | }, 49 | { 50 | Image: "sidecar", 51 | Name: "container", 52 | Resources: v1.ResourceRequirements{ 53 | Requests: v1.ResourceList{ 54 | v1.ResourceCPU: resource.MustParse("1"), 55 | v1.ResourceMemory: resource.MustParse("1Gi"), 56 | }, 57 | }, 58 | RestartPolicy: &restartAlways, 59 | }, 60 | }, 61 | Containers: []v1.Container{ 62 | { 63 | Image: "test-image", 64 | Name: "container", 65 | Resources: v1.ResourceRequirements{ 66 | Requests: v1.ResourceList{ 67 | v1.ResourceCPU: resource.MustParse("1"), 68 | v1.ResourceMemory: resource.MustParse("1Gi"), 69 | }, 70 | }, 71 | }, 72 | }, 73 | }, 74 | } 75 | return p 76 | } 77 | func TestNewPod(t *testing.T) { 78 | pod := testPod("default", "mypod") 79 | pod.Spec.NodeName = "mynode" 80 | p := model.NewPod(pod) 81 | if exp, got := "default", p.Namespace(); exp != got { 82 | t.Errorf("expected Namespace = %s, got %s", exp, got) 83 | } 84 | if exp, got := "mypod", p.Name(); exp != got { 85 | t.Errorf("expected Name = %s, got %s", exp, got) 86 | } 87 | if exp, got := "mynode", p.NodeName(); exp != got { 88 | t.Errorf("expected NodeName = %s, got %s", exp, got) 89 | } 90 | if exp, got := true, p.IsScheduled(); exp != got { 91 | t.Errorf("expected IsScheduled = %v, got %v", exp, got) 92 | } 93 | if exp, got := v1.PodPending, p.Phase(); exp != got { 94 | t.Errorf("expected Phase = %v, got %v", exp, got) 95 | } 96 | 97 | if exp, got := resource.MustParse("2"), p.Requested()[v1.ResourceCPU]; exp.Cmp(got) != 0 { 98 | t.Errorf("expected CPU = %s, got %s", exp.String(), got.String()) 99 | } 100 | if exp, got := resource.MustParse("2Gi"), p.Requested()[v1.ResourceMemory]; exp.Cmp(got) != 0 { 101 | t.Errorf("expected Memory = %s, got %s", exp.String(), got.String()) 102 | } 103 | } 104 | 105 | func TestPodUpdate(t *testing.T) { 106 | p := model.NewPod(testPod("default", "mypod")) 107 | if exp, got := "", p.NodeName(); got != exp { 108 | t.Errorf("expeted NodeName == %s, got %s", exp, got) 109 | } 110 | replacement := testPod("default", "mypod") 111 | replacement.Spec.NodeName = "scheduled.node" 112 | p.Update(replacement) 113 | if exp, got := "scheduled.node", p.NodeName(); got != exp { 114 | t.Errorf("expeted NodeName == %s, got %s", exp, got) 115 | } 116 | } 117 | 118 | func TestFargateCapacity(t *testing.T) { 119 | tp := testPod("default", "mypod") 120 | tp.Annotations = map[string]string{ 121 | "CapacityProvisioned": "0.25vCPU 0.5GB", 122 | } 123 | p := model.NewPod(tp) 124 | cpu, mem, ok := p.FargateCapacityProvisioned() 125 | if !ok { 126 | t.Errorf("expected to have a fargate capacity") 127 | } 128 | if cpu != 0.25 { 129 | t.Errorf("expected to have a cpu capacity of 0.25, got %f", cpu) 130 | } 131 | if mem != 0.5 { 132 | t.Errorf("expected to have a mem capacity of 0.5, got %f", mem) 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /pkg/model/cluster.go: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | 15 | package model 16 | 17 | import ( 18 | "sync" 19 | 20 | v1 "k8s.io/api/core/v1" 21 | ) 22 | 23 | type Cluster struct { 24 | mu sync.RWMutex 25 | nodes map[string]*Node 26 | pods map[objectKey]*Pod 27 | resources []v1.ResourceName 28 | } 29 | 30 | func NewCluster() *Cluster { 31 | return &Cluster{ 32 | nodes: map[string]*Node{}, 33 | pods: map[objectKey]*Pod{}, 34 | resources: []v1.ResourceName{v1.ResourceCPU}, 35 | } 36 | } 37 | func (c *Cluster) AddNode(node *Node) *Node { 38 | c.mu.Lock() 39 | defer c.mu.Unlock() 40 | if existing, ok := c.nodes[node.ProviderID()]; ok { 41 | existing.Update(&node.node) 42 | return existing 43 | } 44 | 45 | c.nodes[node.ProviderID()] = node 46 | return node 47 | } 48 | 49 | func (c *Cluster) DeleteNode(providerID string) { 50 | c.mu.Lock() 51 | defer c.mu.Unlock() 52 | n, ok := c.nodes[providerID] 53 | if !ok { 54 | return 55 | } 56 | var podsToDelete []objectKey 57 | for k, p := range c.pods { 58 | if p.NodeName() == n.node.Name { 59 | podsToDelete = append(podsToDelete, k) 60 | } 61 | } 62 | for _, k := range podsToDelete { 63 | delete(c.pods, k) 64 | } 65 | delete(c.nodes, providerID) 66 | } 67 | 68 | func (c *Cluster) ForEachNode(f func(n *Node)) { 69 | c.mu.RLock() 70 | defer c.mu.RUnlock() 71 | for _, n := range c.nodes { 72 | f(n) 73 | } 74 | } 75 | 76 | func (c *Cluster) GetNode(providerID string) (*Node, bool) { 77 | c.mu.RLock() 78 | defer c.mu.RUnlock() 79 | n, ok := c.nodes[providerID] 80 | return n, ok 81 | } 82 | 83 | func (c *Cluster) GetNodeByName(name string) (*Node, bool) { 84 | c.mu.RLock() 85 | defer c.mu.RUnlock() 86 | for _, n := range c.nodes { 87 | if n.node.Name == name { 88 | return n, true 89 | } 90 | } 91 | return nil, false 92 | } 93 | 94 | func (c *Cluster) AddPod(pod *Pod) (totalPods int) { 95 | c.mu.Lock() 96 | c.pods[objectKey{namespace: pod.Namespace(), name: pod.Name()}] = pod 97 | totalPods = len(c.pods) 98 | c.mu.Unlock() 99 | 100 | if !pod.IsScheduled() { 101 | return 102 | } 103 | n, ok := c.GetNodeByName(pod.NodeName()) 104 | if !ok { 105 | return 106 | } 107 | n.BindPod(pod) 108 | return 109 | } 110 | 111 | func (c *Cluster) DeletePod(namespace, name string) (totalPods int) { 112 | p, ok := c.GetPod(namespace, name) 113 | if ok && p.IsScheduled() { 114 | n, ok := c.GetNodeByName(p.NodeName()) 115 | if ok { 116 | n.DeletePod(namespace, name) 117 | } 118 | } 119 | c.mu.Lock() 120 | delete(c.pods, objectKey{namespace: namespace, name: name}) 121 | totalPods = len(c.pods) 122 | c.mu.Unlock() 123 | return 124 | } 125 | 126 | func (c *Cluster) GetPod(namespace string, name string) (*Pod, bool) { 127 | c.mu.Lock() 128 | pod, ok := c.pods[objectKey{namespace: namespace, name: name}] 129 | c.mu.Unlock() 130 | return pod, ok 131 | } 132 | 133 | func (c *Cluster) Stats() Stats { 134 | c.mu.RLock() 135 | defer c.mu.RUnlock() 136 | st := Stats{ 137 | AllocatableResources: v1.ResourceList{}, 138 | UsedResources: v1.ResourceList{}, 139 | PercentUsedResoruces: map[v1.ResourceName]float64{}, 140 | PodsByPhase: map[v1.PodPhase]int{}, 141 | } 142 | 143 | for _, p := range c.pods { 144 | // skip pods bound to non-visible nodes 145 | if n, ok := c.nodes[p.NodeName()]; ok && !n.Visible() { 146 | continue 147 | } 148 | 149 | st.TotalPods++ 150 | st.PodsByPhase[p.Phase()]++ 151 | if p.NodeName() != "" { 152 | st.BoundPodCount++ 153 | } 154 | } 155 | 156 | for _, n := range c.nodes { 157 | if !n.Visible() { 158 | continue 159 | } 160 | // only add the price if it's not NaN which is used to indicate an unknown 161 | // price 162 | if n.HasPrice() { 163 | st.TotalPrice += n.Price 164 | } 165 | st.NumNodes++ 166 | st.Nodes = append(st.Nodes, n) 167 | addResources(st.AllocatableResources, n.Allocatable()) 168 | addResources(st.UsedResources, n.Used()) 169 | } 170 | return st 171 | } 172 | 173 | // addResources sets lhs = lhs + rhs 174 | func addResources(lhs v1.ResourceList, rhs v1.ResourceList) { 175 | for rn, q := range rhs { 176 | existing := lhs[rn] 177 | existing.Add(q) 178 | lhs[rn] = existing 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /cmd/eks-node-viewer/flag.go: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | package main 15 | 16 | import ( 17 | "bufio" 18 | "errors" 19 | "flag" 20 | "fmt" 21 | "os" 22 | "path/filepath" 23 | "strconv" 24 | "strings" 25 | 26 | "k8s.io/client-go/util/homedir" 27 | ) 28 | 29 | var ( 30 | homeDir string 31 | configPath string 32 | version = "dev" 33 | commit = "" 34 | date = "" 35 | builtBy = "" 36 | ) 37 | 38 | func init() { 39 | homeDir = homedir.HomeDir() 40 | configPath = filepath.Join(homeDir, ".eks-node-viewer") 41 | } 42 | 43 | type Flags struct { 44 | Context string 45 | NodeSelector string 46 | ExtraLabels string 47 | NodeSort string 48 | Style string 49 | Kubeconfig string 50 | Resources string 51 | DisablePricing bool 52 | ShowAttribution bool 53 | Version bool 54 | } 55 | 56 | func ParseFlags() (Flags, error) { 57 | flagSet := flag.NewFlagSet(os.Args[0], flag.ContinueOnError) 58 | var flags Flags 59 | 60 | cfg, err := loadConfigFile() 61 | if err != nil { 62 | return Flags{}, fmt.Errorf("load config file: %w", err) 63 | } 64 | 65 | flagSet.BoolVar(&flags.Version, "v", false, "Display eks-node-viewer version") 66 | flagSet.BoolVar(&flags.Version, "version", false, "Display eks-node-viewer version") 67 | 68 | contextDefault := cfg.getValue("context", "") 69 | flagSet.StringVar(&flags.Context, "context", contextDefault, "Name of the kubernetes context to use") 70 | 71 | nodeSelectorDefault := cfg.getValue("node-selector", "") 72 | flagSet.StringVar(&flags.NodeSelector, "node-selector", nodeSelectorDefault, "Node label selector used to filter nodes, if empty all nodes are selected ") 73 | 74 | extraLabelsDefault := cfg.getValue("extra-labels", "") 75 | flagSet.StringVar(&flags.ExtraLabels, "extra-labels", extraLabelsDefault, "A comma separated set of extra node labels to display") 76 | 77 | nodeSort := cfg.getValue("node-sort", "creation=dsc") 78 | flagSet.StringVar(&flags.NodeSort, "node-sort", nodeSort, "Sort order for the nodes, either 'creation' or a label name. The sort order defaults to ascending and can be controlled by appending =asc or =dsc to the value.") 79 | 80 | style := cfg.getValue("style", "#04B575,#FFFF00,#FF0000") 81 | flagSet.StringVar(&flags.Style, "style", style, "Three color to use for styling 'good','ok' and 'bad' values. These are also used in the gradients displayed from bad -> good.") 82 | 83 | // flag overrides env. var. and env. var. overrides config file 84 | kubeconfigDefault := getStringEnv("KUBECONFIG", cfg.getValue("kubeconfig", filepath.Join(homeDir, ".kube", "config"))) 85 | flagSet.StringVar(&flags.Kubeconfig, "kubeconfig", kubeconfigDefault, "Absolute path to the kubeconfig file") 86 | 87 | resourcesDefault := cfg.getValue("resources", "cpu") 88 | flagSet.StringVar(&flags.Resources, "resources", resourcesDefault, "List of comma separated resources to monitor") 89 | 90 | disablePricingDefault := cfg.getBoolValue("disable-pricing", false) 91 | flagSet.BoolVar(&flags.DisablePricing, "disable-pricing", disablePricingDefault, "Disable pricing lookups") 92 | 93 | flagSet.BoolVar(&flags.ShowAttribution, "attribution", false, "Show the Open Source Attribution") 94 | 95 | if err := flagSet.Parse(os.Args[1:]); err != nil { 96 | return Flags{}, err 97 | } 98 | return flags, nil 99 | } 100 | 101 | // --- env vars --- 102 | 103 | func getStringEnv(envName string, defaultValue string) string { 104 | env, ok := os.LookupEnv(envName) 105 | if !ok { 106 | return defaultValue 107 | } 108 | return env 109 | } 110 | 111 | // --- config file --- 112 | 113 | type configFile map[string]string 114 | 115 | func (c configFile) getValue(key string, defaultValue string) string { 116 | if val, ok := c[key]; ok { 117 | return val 118 | } 119 | return defaultValue 120 | } 121 | 122 | func (c configFile) getBoolValue(key string, defaultValue bool) bool { 123 | if val, ok := c[key]; ok { 124 | if boolVal, err := strconv.ParseBool(val); err == nil { 125 | return boolVal 126 | } 127 | } 128 | return defaultValue 129 | } 130 | 131 | func loadConfigFile() (configFile, error) { 132 | fileContent := make(map[string]string) 133 | if _, err := os.Stat(configPath); errors.Is(err, os.ErrNotExist) { 134 | return fileContent, nil 135 | } 136 | 137 | file, err := os.Open(configPath) 138 | if err != nil { 139 | return nil, err 140 | } 141 | defer file.Close() 142 | 143 | scanner := bufio.NewScanner(file) 144 | for scanner.Scan() { 145 | line := strings.TrimSpace(scanner.Text()) 146 | if strings.HasPrefix(line, "#") { 147 | continue 148 | } 149 | lineKV := strings.SplitN(line, "=", 2) 150 | if len(lineKV) == 2 { 151 | key := strings.TrimSpace(lineKV[0]) 152 | value := strings.TrimSpace(lineKV[1]) 153 | fileContent[key] = value 154 | } 155 | } 156 | 157 | if err := scanner.Err(); err != nil { 158 | return nil, err 159 | } 160 | return fileContent, nil 161 | } 162 | -------------------------------------------------------------------------------- /pkg/model/node_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | package model_test 15 | 16 | import ( 17 | "testing" 18 | "time" 19 | 20 | v1 "k8s.io/api/core/v1" 21 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 22 | 23 | "github.com/awslabs/eks-node-viewer/pkg/model" 24 | ) 25 | 26 | func testNode(name string) *v1.Node { 27 | n := &v1.Node{ 28 | ObjectMeta: metav1.ObjectMeta{ 29 | Name: name, 30 | }, 31 | Status: v1.NodeStatus{ 32 | Phase: v1.NodePending, 33 | }, 34 | } 35 | return n 36 | } 37 | 38 | func TestNewNode(t *testing.T) { 39 | n := testNode("mynode") 40 | node := model.NewNode(n) 41 | if exp, got := "mynode", node.Name(); exp != got { 42 | t.Errorf("expeted Name == %s, got %s", exp, got) 43 | } 44 | } 45 | 46 | func TestNodeTypeUnknown(t *testing.T) { 47 | n := testNode("mynode") 48 | node := model.NewNode(n) 49 | if node.IsOnDemand() { 50 | t.Errorf("exepcted to not be on-demand") 51 | } 52 | if node.IsSpot() { 53 | t.Errorf("exepcted to not be spot") 54 | } 55 | } 56 | 57 | func TestNodeTypeOnDemand(t *testing.T) { 58 | for label, value := range map[string]string{ 59 | "karpenter.sh/capacity-type": "on-demand", 60 | "eks.amazonaws.com/capacityType": "ON_DEMAND", 61 | } { 62 | n := testNode("mynode") 63 | n.Labels = map[string]string{ 64 | label: value, 65 | } 66 | node := model.NewNode(n) 67 | if !node.IsOnDemand() { 68 | t.Errorf("exepcted on-demand") 69 | } 70 | if node.IsSpot() { 71 | t.Errorf("exepcted to not be spot") 72 | } 73 | if node.IsFargate() { 74 | t.Errorf("exepcted to not be fargate") 75 | } 76 | } 77 | } 78 | 79 | func TestNodeTypeSpot(t *testing.T) { 80 | for label, value := range map[string]string{ 81 | "karpenter.sh/capacity-type": "spot", 82 | "eks.amazonaws.com/capacityType": "SPOT", 83 | } { 84 | n := testNode("mynode") 85 | n.Labels = map[string]string{ 86 | label: value, 87 | } 88 | node := model.NewNode(n) 89 | if node.IsOnDemand() { 90 | t.Errorf("exepcted to not be on-demand") 91 | } 92 | if !node.IsSpot() { 93 | t.Errorf("exepcted to be spot") 94 | } 95 | if node.IsFargate() { 96 | t.Errorf("exepcted to not be fargate") 97 | } 98 | } 99 | } 100 | 101 | func TestNodeTypeFargate(t *testing.T) { 102 | for label, value := range map[string]string{ 103 | "eks.amazonaws.com/compute-type": "fargate", 104 | } { 105 | n := testNode("mynode") 106 | n.Labels = map[string]string{ 107 | label: value, 108 | } 109 | node := model.NewNode(n) 110 | if node.IsOnDemand() { 111 | t.Errorf("exepcted to not be on-demand") 112 | } 113 | if node.IsSpot() { 114 | t.Errorf("exepcted to not be spot") 115 | } 116 | if !node.IsFargate() { 117 | t.Errorf("exepcted to be fargate") 118 | } 119 | } 120 | } 121 | 122 | func TestNodeTypeAuto(t *testing.T) { 123 | for label, value := range map[string]string{ 124 | "eks.amazonaws.com/compute-type": "auto", 125 | } { 126 | n := testNode("mynode") 127 | n.Labels = map[string]string{ 128 | label: value, 129 | } 130 | node := model.NewNode(n) 131 | if node.IsOnDemand() { 132 | t.Errorf("exepcted to not be on-demand") 133 | } 134 | if node.IsSpot() { 135 | t.Errorf("exepcted to not be spot") 136 | } 137 | if node.IsFargate() { 138 | t.Errorf("exepcted to not be fargate") 139 | } 140 | if !node.IsAuto() { 141 | t.Errorf("exepcted to be auto") 142 | } 143 | } 144 | } 145 | 146 | func TestNodeNotReadyFalse(t *testing.T) { 147 | for _, status := range []v1.ConditionStatus{v1.ConditionFalse, v1.ConditionUnknown} { 148 | t.Run(string(status), func(t *testing.T) { 149 | n := testNode("mynode") 150 | n.Status.Phase = v1.NodeRunning 151 | notReadyTime := time.Now().Add(-1 * time.Hour) 152 | 153 | n.Status.Conditions = append(n.Status.Conditions, v1.NodeCondition{ 154 | Type: v1.NodeReady, 155 | Status: status, 156 | LastTransitionTime: metav1.Time{ 157 | Time: notReadyTime, 158 | }, 159 | }) 160 | node := model.NewNode(n) 161 | if node.Ready() { 162 | t.Fatalf("expected node to be not ready") 163 | } 164 | 165 | if node.NotReadyTime() != notReadyTime { 166 | t.Errorf("expected not ready time = %s, got %s", notReadyTime, node.NotReadyTime()) 167 | } 168 | }) 169 | } 170 | } 171 | 172 | func TestNodeNotReadyNoCondition(t *testing.T) { 173 | for _, status := range []v1.ConditionStatus{v1.ConditionFalse, v1.ConditionUnknown} { 174 | t.Run(string(status), func(t *testing.T) { 175 | n := testNode("mynode") 176 | n.Status.Phase = v1.NodeRunning 177 | notReadyTime := time.Now().Add(-1 * time.Hour) 178 | n.CreationTimestamp = metav1.NewTime(notReadyTime) 179 | 180 | node := model.NewNode(n) 181 | if node.Ready() { 182 | t.Fatalf("expected node to be not ready") 183 | } 184 | 185 | if node.NotReadyTime() != notReadyTime { 186 | t.Errorf("expected not ready time = %s, got %s", notReadyTime, node.NotReadyTime()) 187 | } 188 | }) 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![GitHub License](https://img.shields.io/badge/License-Apache%202.0-ff69b4.svg)](https://github.com/awslabs/eks-node-viewer/blob/main/LICENSE) 2 | [![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](https://github.com/awslabs/eks-node-viewer/issues) 3 | [![Go code tests](https://github.com/awslabs/eks-node-viewer/actions/workflows/test.yaml/badge.svg)](https://github.com/awslabs/eks-node-viewer/actions/workflows/test.yaml) 4 | 5 | ## Usage 6 | 7 | `eks-node-viewer` is a tool for visualizing dynamic node usage within a cluster. It was originally developed as an internal tool at AWS for demonstrating consolidation with [Karpenter](https://karpenter.sh/). It displays the scheduled pod resource requests vs the allocatable capacity on the node. It *does not* look at the actual pod resource usage. 8 | 9 | ![](./.static/screenshot.png) 10 | 11 | ### Talks Using eks-node-viewer 12 | 13 | - [Containers from the Couch: Workload Consolidation with Karpenter](https://www.youtube.com/watch?v=BnksdJ3oOEs) 14 | - [AWS re:Invent 2022 - Kubernetes virtually anywhere, for everyone](https://www.youtube.com/watch?v=OB7IZolZk78) 15 | 16 | ### Installation 17 | 18 | #### Homebrew 19 | 20 | ```bash 21 | brew tap aws/tap 22 | brew install eks-node-viewer 23 | ``` 24 | 25 | #### Manual 26 | Please either fetch the latest [release](https://github.com/awslabs/eks-node-viewer/releases) or install manually using: 27 | ```shell 28 | go install github.com/awslabs/eks-node-viewer/cmd/eks-node-viewer@latest 29 | ``` 30 | 31 | Note: This will install it to your `GOBIN` directory, typically `~/go/bin` if it is unconfigured. 32 | 33 | ## Usage 34 | ```shell 35 | Usage of ./eks-node-viewer: 36 | -attribution 37 | Show the Open Source Attribution 38 | -context string 39 | Name of the kubernetes context to use 40 | -disable-pricing 41 | Disable pricing lookups 42 | -extra-labels string 43 | A comma separated set of extra node labels to display 44 | -kubeconfig string 45 | Absolute path to the kubeconfig file (default "~/.kube/config") 46 | -node-selector string 47 | Node label selector used to filter nodes, if empty all nodes are selected 48 | -node-sort string 49 | Sort order for the nodes, either 'creation' or a label name. The sort order can be controlled by appending =asc or =dsc to the value. (default "creation") 50 | -resources string 51 | List of comma separated resources to monitor (default "cpu") 52 | -style string 53 | Three color to use for styling 'good','ok' and 'bad' values. These are also used in the gradients displayed from bad -> good. (default "#04B575,#FFFF00,#FF0000") 54 | -v Display eks-node-viewer version 55 | -version 56 | Display eks-node-viewer version 57 | ``` 58 | 59 | ### Examples 60 | ```shell 61 | # Standard usage 62 | eks-node-viewer 63 | # Karpenter nodes only 64 | eks-node-viewer --node-selector karpenter.sh/nodepool 65 | # Display both CPU and Memory Usage 66 | eks-node-viewer --resources cpu,memory 67 | # Display extra labels, i.e. AZ 68 | eks-node-viewer --extra-labels topology.kubernetes.io/zone 69 | # Sort by CPU usage in descending order 70 | eks-node-viewer --node-sort=eks-node-viewer/node-cpu-usage=dsc 71 | # Specify a particular AWS profile and region 72 | AWS_PROFILE=myprofile AWS_REGION=us-west-2 73 | ``` 74 | 75 | ### Computed Labels 76 | 77 | `eks-node-viewer` supports some custom label names that can be passed to the `--extra-labels` to display additional node information. 78 | 79 | - `eks-node-viewer/node-age` - Age of the node 80 | - `eks-node-viewer/node-cpu-usage` - CPU usage (requests) 81 | - `eks-node-viewer/node-memory-usage` - Memory usage (requests) 82 | - `eks-node-viewer/node-pods-usage` - Pod usage (requests) 83 | - `eks-node-viewer/node-ephemeral-storage-usage` - Ephemeral Storage usage (requests) 84 | 85 | ### Default Options 86 | You can supply default options to `eks-node-viewer` by creating a file named `.eks-node-viewer` in your home directory and specifying 87 | options there. The format is `option-name=value` where the option names are the command line flags: 88 | ```text 89 | # select only Karpenter managed nodes 90 | node-selector=karpenter.sh/nodepool 91 | 92 | # display both CPU and memory 93 | resources=cpu,memory 94 | 95 | # show the zone and nodepool name by default 96 | extra-labels=topology.kubernetes.io/zone,karpenter.sh/nodepool 97 | 98 | # sort so that the newest nodes are first 99 | node-sort=creation=asc 100 | 101 | # change default color style 102 | style=#2E91D2,#ffff00,#D55E00 103 | ``` 104 | 105 | ### Troubleshooting 106 | 107 | #### NoCredentialProviders: no valid providers in chain. Deprecated. 108 | 109 | This CLI relies on AWS credentials to access pricing data if you don't use the `--disable-pricing` option. You must have credentials configured via `~/aws/credentials`, `~/.aws/config`, environment variables, or some other credential provider chain. 110 | 111 | See [credential provider documentation](https://docs.aws.amazon.com/sdk-for-go/api/aws/session/) for more. 112 | 113 | #### I get an error of `creating client, exec plugin: invalid apiVersion "client.authentication.k8s.io/v1alpha1"` 114 | 115 | Updating your AWS cli to the latest version and [updating your kubeconfig](https://docs.aws.amazon.com/cli/latest/reference/eks/update-kubeconfig.html) should resolve this issue. 116 | 117 | ## Development 118 | 119 | ### Building 120 | 121 | ```shell 122 | $ make build 123 | ``` 124 | 125 | Or local execution of GoReleaser build: 126 | ```shell 127 | $ make goreleaser 128 | ``` 129 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/awslabs/eks-node-viewer 2 | 3 | go 1.24.6 4 | 5 | require ( 6 | github.com/aws/aws-sdk-go-v2 v1.40.1 7 | github.com/aws/aws-sdk-go-v2/config v1.32.3 8 | github.com/aws/aws-sdk-go-v2/service/ec2 v1.275.1 9 | github.com/aws/aws-sdk-go-v2/service/pricing v1.40.8 10 | github.com/charmbracelet/bubbles v0.21.0 11 | github.com/charmbracelet/bubbletea v1.3.10 12 | github.com/charmbracelet/lipgloss v1.1.0 13 | github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb 14 | go.uber.org/multierr v1.11.0 15 | golang.org/x/text v0.32.0 16 | k8s.io/api v0.34.2 17 | k8s.io/apimachinery v0.34.2 18 | k8s.io/client-go v0.34.2 19 | sigs.k8s.io/karpenter v1.8.0 20 | ) 21 | 22 | require ( 23 | github.com/aws/aws-sdk-go-v2/credentials v1.19.3 // indirect 24 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.15 // indirect 25 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.15 // indirect 26 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.15 // indirect 27 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect 28 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect 29 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.15 // indirect 30 | github.com/aws/aws-sdk-go-v2/service/signin v1.0.3 // indirect 31 | github.com/aws/aws-sdk-go-v2/service/sso v1.30.6 // indirect 32 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.11 // indirect 33 | github.com/aws/aws-sdk-go-v2/service/sts v1.41.3 // indirect 34 | github.com/aws/smithy-go v1.24.0 // indirect 35 | github.com/awslabs/operatorpkg v0.0.0-20250909182303-e8e550b6f339 // indirect 36 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect 37 | github.com/beorn7/perks v1.0.1 // indirect 38 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 39 | github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect 40 | github.com/charmbracelet/harmonica v0.2.0 // indirect 41 | github.com/charmbracelet/x/ansi v0.10.1 // indirect 42 | github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect 43 | github.com/charmbracelet/x/term v0.2.1 // indirect 44 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 45 | github.com/emicklei/go-restful/v3 v3.12.2 // indirect 46 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect 47 | github.com/evanphx/json-patch/v5 v5.9.11 // indirect 48 | github.com/fsnotify/fsnotify v1.9.0 // indirect 49 | github.com/fxamacker/cbor/v2 v2.9.0 // indirect 50 | github.com/go-logr/logr v1.4.3 // indirect 51 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 52 | github.com/go-openapi/jsonreference v0.20.2 // indirect 53 | github.com/go-openapi/swag v0.23.0 // indirect 54 | github.com/gogo/protobuf v1.3.2 // indirect 55 | github.com/google/btree v1.1.3 // indirect 56 | github.com/google/gnostic-models v0.7.0 // indirect 57 | github.com/google/go-cmp v0.7.0 // indirect 58 | github.com/google/uuid v1.6.0 // indirect 59 | github.com/josharian/intern v1.0.0 // indirect 60 | github.com/json-iterator/go v1.1.12 // indirect 61 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 62 | github.com/mailru/easyjson v0.7.7 // indirect 63 | github.com/mattn/go-isatty v0.0.20 // indirect 64 | github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75 // indirect 65 | github.com/mattn/go-runewidth v0.0.16 // indirect 66 | github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect 67 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 68 | github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect 69 | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect 70 | github.com/muesli/cancelreader v0.2.2 // indirect 71 | github.com/muesli/termenv v0.16.0 // indirect 72 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 73 | github.com/pkg/errors v0.9.1 // indirect 74 | github.com/pmezard/go-difflib v1.0.0 // indirect 75 | github.com/prometheus/client_golang v1.23.2 // indirect 76 | github.com/prometheus/client_model v0.6.2 // indirect 77 | github.com/prometheus/common v0.66.1 // indirect 78 | github.com/prometheus/procfs v0.16.1 // indirect 79 | github.com/rivo/uniseg v0.4.7 // indirect 80 | github.com/robfig/cron/v3 v3.0.1 // indirect 81 | github.com/samber/lo v1.51.0 // indirect 82 | github.com/spf13/pflag v1.0.10 // indirect 83 | github.com/x448/float16 v0.8.4 // indirect 84 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect 85 | go.yaml.in/yaml/v2 v2.4.2 // indirect 86 | go.yaml.in/yaml/v3 v3.0.4 // indirect 87 | golang.org/x/net v0.44.0 // indirect 88 | golang.org/x/oauth2 v0.30.0 // indirect 89 | golang.org/x/sync v0.19.0 // indirect 90 | golang.org/x/sys v0.36.0 // indirect 91 | golang.org/x/term v0.35.0 // indirect 92 | golang.org/x/time v0.13.0 // indirect 93 | gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect 94 | google.golang.org/protobuf v1.36.8 // indirect 95 | gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect 96 | gopkg.in/inf.v0 v0.9.1 // indirect 97 | gopkg.in/yaml.v3 v3.0.1 // indirect 98 | k8s.io/apiextensions-apiserver v0.34.1 // indirect 99 | k8s.io/klog/v2 v2.130.1 // indirect 100 | k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect 101 | k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect 102 | sigs.k8s.io/controller-runtime v0.22.1 // indirect 103 | sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect 104 | sigs.k8s.io/randfill v1.0.0 // indirect 105 | sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect 106 | sigs.k8s.io/yaml v1.6.0 // indirect 107 | ) 108 | -------------------------------------------------------------------------------- /pkg/model/cluster_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | package model_test 15 | 16 | import ( 17 | "testing" 18 | 19 | v1 "k8s.io/api/core/v1" 20 | "k8s.io/apimachinery/pkg/api/resource" 21 | 22 | "github.com/awslabs/eks-node-viewer/pkg/model" 23 | ) 24 | 25 | func TestClusterAddNode(t *testing.T) { 26 | cluster := model.NewCluster() 27 | 28 | if got := len(cluster.Stats().Nodes); got != 0 { 29 | t.Errorf("expected 0 nodes, got %d", got) 30 | } 31 | nodeCount := 0 32 | cluster.ForEachNode(func(n *model.Node) { 33 | nodeCount++ 34 | }) 35 | if got := nodeCount; got != 0 { 36 | t.Errorf("expected to iterate over 0 nodes, had %d", got) 37 | } 38 | 39 | n := testNode("mynode") 40 | node := model.NewNode(n) 41 | cluster.AddNode(node) 42 | 43 | // doesn't show not-visible node 44 | if got := len(cluster.Stats().Nodes); got != 0 { 45 | t.Errorf("expected 0 nodes, got %d", got) 46 | } 47 | 48 | // but is iterable 49 | cluster.ForEachNode(func(n *model.Node) { 50 | nodeCount++ 51 | }) 52 | if got := nodeCount; got != 1 { 53 | t.Errorf("expected to iterate over 1 node, had %d", got) 54 | } 55 | 56 | // making the node visible causes it to appear in stats 57 | node.Show() 58 | if got := len(cluster.Stats().Nodes); got != 1 { 59 | t.Errorf("expected 1 nodes, got %d", got) 60 | 61 | } 62 | 63 | } 64 | 65 | func TestClusterGetNodeByProviderID(t *testing.T) { 66 | cluster := model.NewCluster() 67 | 68 | _, ok := cluster.GetNode("mynode-id") 69 | if ok { 70 | t.Errorf("expected to not find node") 71 | } 72 | n := testNode("mynode") 73 | n.Spec.ProviderID = "mynode-id" 74 | node := model.NewNode(n) 75 | cluster.AddNode(node) 76 | 77 | _, ok = cluster.GetNode("mynode-id") 78 | if !ok { 79 | t.Errorf("expected to find node by provider id") 80 | } 81 | 82 | // delete and we should fail to find it 83 | cluster.DeleteNode("mynode-id") 84 | _, ok = cluster.GetNode("mynode-id") 85 | if ok { 86 | t.Errorf("expected to not find node after deletion") 87 | } 88 | } 89 | 90 | func TestClusterGetNodeByName(t *testing.T) { 91 | cluster := model.NewCluster() 92 | 93 | _, ok := cluster.GetNodeByName("mynode") 94 | if ok { 95 | t.Errorf("expected to not find node") 96 | } 97 | n := testNode("mynode") 98 | node := model.NewNode(n) 99 | cluster.AddNode(node) 100 | 101 | _, ok = cluster.GetNodeByName("mynode") 102 | if !ok { 103 | t.Errorf("expected to find node by name") 104 | } 105 | } 106 | 107 | func TestClusterUpdateNode(t *testing.T) { 108 | cluster := model.NewCluster() 109 | 110 | n1 := testNode("mynode") 111 | n1.Status.Allocatable = v1.ResourceList{ 112 | "cpu": resource.MustParse("1"), 113 | } 114 | node1 := model.NewNode(n1) 115 | node1.Show() 116 | cluster.AddNode(node1) 117 | 118 | if got := cluster.Stats().AllocatableResources["cpu"]; got.Cmp(resource.MustParse("1")) != 0 { 119 | t.Errorf("expected total CPU = 1, got %s", got.String()) 120 | } 121 | 122 | // simulate a node update 123 | n2 := testNode("mynode") 124 | n2.Status.Allocatable = v1.ResourceList{ 125 | "cpu": resource.MustParse("2"), 126 | } 127 | node2 := model.NewNode(n2) 128 | node2.Show() 129 | cluster.AddNode(node2) 130 | 131 | if got := cluster.Stats().AllocatableResources["cpu"]; got.Cmp(resource.MustParse("2")) != 0 { 132 | t.Errorf("expected total CPU = 2, got %s", got.String()) 133 | } 134 | 135 | } 136 | 137 | func TestClusterAddPod(t *testing.T) { 138 | cluster := model.NewCluster() 139 | 140 | n := testNode("mynode") 141 | n.Spec.ProviderID = "mynode-id" 142 | node := model.NewNode(n) 143 | node.Show() 144 | cluster.AddNode(node) 145 | 146 | if got := cluster.Stats().TotalPods; got != 0 { 147 | t.Errorf("expected 0 pods, got %d", got) 148 | } 149 | if got := cluster.Stats().UsedResources["cpu"]; got.Cmp(resource.MustParse("0")) != 0 { 150 | t.Errorf("expected 0 CPU used, got %s", got.String()) 151 | } 152 | 153 | p := testPod("default", "mypod") 154 | p.Spec.NodeName = n.Name 155 | pod := model.NewPod(p) 156 | cluster.AddPod(pod) 157 | 158 | if got := cluster.Stats().TotalPods; got != 1 { 159 | t.Errorf("expected 0 pods, got %d", got) 160 | } 161 | 162 | if got := cluster.Stats().UsedResources["cpu"]; got.Cmp(resource.MustParse("2")) != 0 { 163 | t.Errorf("expected 2 CPU used, got %s", got.String()) 164 | } 165 | 166 | // deleting the pod should remove the usage 167 | cluster.DeletePod("default", "mypod") 168 | if got := cluster.Stats().TotalPods; got != 0 { 169 | t.Errorf("expected 0 pods, got %d", got) 170 | } 171 | if got := cluster.Stats().UsedResources["cpu"]; got.Cmp(resource.MustParse("0")) != 0 { 172 | t.Errorf("expected 0 CPU used, got %s", got.String()) 173 | } 174 | 175 | } 176 | 177 | func TestClusterDeleteNodeDeletesPods(t *testing.T) { 178 | cluster := model.NewCluster() 179 | 180 | // add a node and pod bound to that node 181 | n := testNode("mynode") 182 | n.Spec.ProviderID = "mynode-id" 183 | node := model.NewNode(n) 184 | node.Show() 185 | cluster.AddNode(node) 186 | 187 | p := testPod("default", "mypod") 188 | p.Spec.NodeName = n.Name 189 | pod := model.NewPod(p) 190 | cluster.AddPod(pod) 191 | 192 | // verify we are tracking usage 193 | if got := cluster.Stats().TotalPods; got != 1 { 194 | t.Errorf("expected 0 pods, got %d", got) 195 | } 196 | 197 | if got := cluster.Stats().UsedResources["cpu"]; got.Cmp(resource.MustParse("2")) != 0 { 198 | t.Errorf("expected 2 CPU used, got %s", got.String()) 199 | } 200 | 201 | // deleting the node should clear all of the usage of pods that were bound to the node 202 | cluster.DeleteNode("mynode-id") 203 | 204 | if got := cluster.Stats().TotalPods; got != 0 { 205 | t.Errorf("expected 0 pods, got %d", got) 206 | } 207 | if got := cluster.Stats().UsedResources["cpu"]; got.Cmp(resource.MustParse("0")) != 0 { 208 | t.Errorf("expected 0 CPU used, got %s", got.String()) 209 | } 210 | 211 | } 212 | -------------------------------------------------------------------------------- /pkg/client/controller.go: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | package client 15 | 16 | import ( 17 | "context" 18 | "math" 19 | "strconv" 20 | "time" 21 | 22 | v1 "k8s.io/api/core/v1" 23 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 24 | "k8s.io/apimachinery/pkg/fields" 25 | "k8s.io/apimachinery/pkg/labels" 26 | "k8s.io/client-go/kubernetes" 27 | "k8s.io/client-go/rest" 28 | "k8s.io/client-go/tools/cache" 29 | karpv1 "sigs.k8s.io/karpenter/pkg/apis/v1" 30 | 31 | "github.com/awslabs/eks-node-viewer/pkg/model" 32 | "github.com/awslabs/eks-node-viewer/pkg/pricing" 33 | ) 34 | 35 | type Controller struct { 36 | kubeClient *kubernetes.Clientset 37 | uiModel *model.UIModel 38 | pricing pricing.Provider 39 | nodeSelector labels.Selector 40 | nodeClaimClient *rest.RESTClient 41 | } 42 | 43 | func NewController(kubeClient *kubernetes.Clientset, nodeClaimClient *rest.RESTClient, uiModel *model.UIModel, nodeSelector labels.Selector, pricing pricing.Provider) *Controller { 44 | c := &Controller{ 45 | kubeClient: kubeClient, 46 | uiModel: uiModel, 47 | pricing: pricing, 48 | nodeSelector: nodeSelector, 49 | nodeClaimClient: nodeClaimClient, 50 | } 51 | pricing.OnUpdate(c.RefreshNodePrices) 52 | return c 53 | } 54 | 55 | func (m Controller) Start(ctx context.Context) { 56 | cluster := m.uiModel.Cluster() 57 | 58 | m.startPodWatch(ctx, cluster) 59 | m.startNodeWatch(ctx, cluster) 60 | 61 | // If a NodeClaims Get returns an error, then don't startup the nodeclaims controller since the CRD is not registered 62 | if err := m.nodeClaimClient.Get().Do(ctx).Error(); err == nil { 63 | m.startNodeClaimWatch(ctx, cluster) 64 | } 65 | } 66 | 67 | func (m Controller) startNodeClaimWatch(ctx context.Context, cluster *model.Cluster) { 68 | nodeClaimWatchList := cache.NewFilteredListWatchFromClient(m.nodeClaimClient, "nodeclaims", 69 | v1.NamespaceAll, func(options *metav1.ListOptions) { 70 | options.LabelSelector = m.nodeSelector.String() 71 | }) 72 | _, nodeClaimController := cache.NewInformer( 73 | nodeClaimWatchList, 74 | &karpv1.NodeClaim{}, 75 | time.Second*0, 76 | cache.ResourceEventHandlerFuncs{ 77 | AddFunc: func(obj interface{}) { 78 | nc := obj.(*karpv1.NodeClaim) 79 | if nc.Status.ProviderID == "" { 80 | return 81 | } 82 | if _, ok := cluster.GetNode(nc.Status.ProviderID); ok { 83 | return 84 | } 85 | node := model.NewNodeFromNodeClaim(nc) 86 | m.updatePrice(node) 87 | n := cluster.AddNode(node) 88 | n.Show() 89 | }, 90 | DeleteFunc: func(obj interface{}) { 91 | cluster.DeleteNode(ignoreDeletedFinalStateUnknown(obj).(*karpv1.NodeClaim).Status.ProviderID) 92 | }, 93 | UpdateFunc: func(oldObj, newObj interface{}) { 94 | nc := newObj.(*karpv1.NodeClaim) 95 | if nc.Status.ProviderID == "" { 96 | return 97 | } 98 | if _, ok := cluster.GetNode(nc.Status.ProviderID); ok { 99 | return 100 | } 101 | node := model.NewNodeFromNodeClaim(nc) 102 | m.updatePrice(node) 103 | n := cluster.AddNode(node) 104 | n.Show() 105 | }, 106 | }, 107 | ) 108 | go nodeClaimController.Run(ctx.Done()) 109 | } 110 | 111 | func (m Controller) startNodeWatch(ctx context.Context, cluster *model.Cluster) { 112 | nodeWatchList := cache.NewFilteredListWatchFromClient(m.kubeClient.CoreV1().RESTClient(), "nodes", 113 | v1.NamespaceAll, func(options *metav1.ListOptions) { 114 | options.LabelSelector = m.nodeSelector.String() 115 | }) 116 | _, nodeController := cache.NewInformer( 117 | nodeWatchList, 118 | &v1.Node{}, 119 | time.Second*0, 120 | cache.ResourceEventHandlerFuncs{ 121 | AddFunc: func(obj interface{}) { 122 | node := model.NewNode(obj.(*v1.Node)) 123 | if node.ProviderID() == "" { 124 | return 125 | } 126 | m.updatePrice(node) 127 | n := cluster.AddNode(node) 128 | n.Show() 129 | }, 130 | DeleteFunc: func(obj interface{}) { 131 | cluster.DeleteNode(ignoreDeletedFinalStateUnknown(obj).(*v1.Node).Spec.ProviderID) 132 | }, 133 | UpdateFunc: func(oldObj, newObj interface{}) { 134 | n := newObj.(*v1.Node) 135 | if !n.DeletionTimestamp.IsZero() && len(n.Finalizers) == 0 { 136 | cluster.DeleteNode(n.Spec.ProviderID) 137 | } else { 138 | node, ok := cluster.GetNode(n.Spec.ProviderID) 139 | if ok { 140 | node.Update(n) 141 | m.updatePrice(node) 142 | node.Show() 143 | } 144 | } 145 | }, 146 | }, 147 | ) 148 | go nodeController.Run(ctx.Done()) 149 | } 150 | 151 | func (m Controller) startPodWatch(ctx context.Context, cluster *model.Cluster) { 152 | podWatchList := cache.NewListWatchFromClient(m.kubeClient.CoreV1().RESTClient(), "pods", 153 | v1.NamespaceAll, fields.Everything()) 154 | 155 | _, podController := cache.NewInformer( 156 | podWatchList, 157 | &v1.Pod{}, 158 | time.Second*0, 159 | cache.ResourceEventHandlerFuncs{ 160 | AddFunc: func(obj interface{}) { 161 | p := obj.(*v1.Pod) 162 | if !isTerminalPod(p) { 163 | cluster.AddPod(model.NewPod(p)) 164 | node, ok := cluster.GetNodeByName(p.Spec.NodeName) 165 | // need to potentially update node price as we need the fargate pod in order to figure out the cost 166 | if ok && node.IsFargate() && !node.HasPrice() { 167 | m.updatePrice(node) 168 | } 169 | } 170 | }, 171 | DeleteFunc: func(obj interface{}) { 172 | p := ignoreDeletedFinalStateUnknown(obj).(*v1.Pod) 173 | cluster.DeletePod(p.Namespace, p.Name) 174 | }, 175 | UpdateFunc: func(oldObj, newObj interface{}) { 176 | p := newObj.(*v1.Pod) 177 | if isTerminalPod(p) { 178 | cluster.DeletePod(p.Namespace, p.Name) 179 | } else { 180 | pod, ok := cluster.GetPod(p.Namespace, p.Name) 181 | if !ok { 182 | cluster.AddPod(model.NewPod(p)) 183 | } else { 184 | pod.Update(p) 185 | cluster.AddPod(pod) 186 | } 187 | } 188 | }, 189 | }, 190 | ) 191 | go podController.Run(ctx.Done()) 192 | } 193 | 194 | func (m Controller) updatePrice(node *model.Node) { 195 | // If the node has the instance-price override label, don't look up pricing 196 | // and use the value here. 197 | if val, ok := node.Labels()["eks-node-viewer/instance-price"]; ok { 198 | if price, err := strconv.ParseFloat(val, 64); err == nil { 199 | node.SetPrice(price) 200 | return 201 | } 202 | } 203 | // lookup our n price 204 | node.Price = math.NaN() 205 | if price, ok := m.pricing.NodePrice(node); ok { 206 | node.SetPrice(price) 207 | } 208 | 209 | } 210 | 211 | func (m Controller) RefreshNodePrices() { 212 | m.uiModel.Cluster().ForEachNode(func(n *model.Node) { 213 | m.updatePrice(n) 214 | }) 215 | } 216 | 217 | // isTerminalPod returns true if the pod is deleting or in a terminal state 218 | func isTerminalPod(p *v1.Pod) bool { 219 | if !p.DeletionTimestamp.IsZero() { 220 | return true 221 | } 222 | switch p.Status.Phase { 223 | case v1.PodSucceeded, v1.PodFailed: 224 | return true 225 | } 226 | return false 227 | } 228 | 229 | // ignoreDeletedFinalStateUnknown returns the object wrapped in 230 | // DeletedFinalStateUnknown. Useful in OnDelete resource event handlers that do 231 | // not need the additional context. 232 | func ignoreDeletedFinalStateUnknown(obj interface{}) interface{} { 233 | if obj, ok := obj.(cache.DeletedFinalStateUnknown); ok { 234 | return obj.Obj 235 | } 236 | return obj 237 | } 238 | -------------------------------------------------------------------------------- /pkg/model/node.go: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | 15 | package model 16 | 17 | import ( 18 | "fmt" 19 | "regexp" 20 | "sync" 21 | "time" 22 | 23 | ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types" 24 | v1 "k8s.io/api/core/v1" 25 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 | "k8s.io/apimachinery/pkg/util/duration" 27 | karpv1 "sigs.k8s.io/karpenter/pkg/apis/v1" 28 | ) 29 | 30 | var ( 31 | instanceIDRegex = regexp.MustCompile(`aws:///(?P.*)/(?P.*)`) 32 | ) 33 | 34 | type objectKey struct { 35 | namespace string 36 | name string 37 | } 38 | type Node struct { 39 | mu sync.RWMutex 40 | visible bool 41 | node v1.Node 42 | pods map[objectKey]*Pod 43 | used v1.ResourceList 44 | Price float64 45 | nodeclaimCreationTime time.Time 46 | } 47 | 48 | func NewNode(n *v1.Node) *Node { 49 | node := &Node{ 50 | node: *n, 51 | pods: map[objectKey]*Pod{}, 52 | used: v1.ResourceList{}, 53 | } 54 | 55 | return node 56 | } 57 | 58 | func NewNodeFromNodeClaim(nc *karpv1.NodeClaim) *Node { 59 | node := NewNode(&v1.Node{ 60 | ObjectMeta: metav1.ObjectMeta{ 61 | Name: nc.Status.NodeName, 62 | CreationTimestamp: nc.CreationTimestamp, 63 | Labels: nc.Labels, 64 | Annotations: nc.Annotations, 65 | }, 66 | Spec: v1.NodeSpec{ 67 | Taints: nc.Spec.Taints, 68 | ProviderID: nc.Status.ProviderID, 69 | }, 70 | Status: v1.NodeStatus{ 71 | Capacity: nc.Status.Capacity, 72 | Allocatable: nc.Status.Allocatable, 73 | }, 74 | }) 75 | node.nodeclaimCreationTime = nc.CreationTimestamp.Time 76 | return node 77 | } 78 | 79 | func (n *Node) IsOnDemand() bool { 80 | return n.node.Labels["karpenter.sh/capacity-type"] == "on-demand" || 81 | n.node.Labels["eks.amazonaws.com/capacityType"] == "ON_DEMAND" 82 | } 83 | 84 | func (n *Node) IsSpot() bool { 85 | return n.node.Labels["karpenter.sh/capacity-type"] == "spot" || 86 | n.node.Labels["eks.amazonaws.com/capacityType"] == "SPOT" 87 | } 88 | 89 | func (n *Node) IsFargate() bool { 90 | return n.node.Labels["eks.amazonaws.com/compute-type"] == "fargate" 91 | } 92 | 93 | func (n *Node) IsAuto() bool { 94 | return n.node.Labels["eks.amazonaws.com/compute-type"] == "auto" 95 | } 96 | 97 | func (n *Node) Labels() map[string]string { 98 | return n.node.Labels 99 | } 100 | 101 | func (n *Node) Update(node *v1.Node) { 102 | n.mu.Lock() 103 | defer n.mu.Unlock() 104 | n.node = *node 105 | } 106 | 107 | func (n *Node) Name() string { 108 | n.mu.RLock() 109 | defer n.mu.RUnlock() 110 | if n.node.Name == "" { 111 | return n.InstanceID() 112 | } 113 | return n.node.Name 114 | } 115 | 116 | func (n *Node) ProviderID() string { 117 | n.mu.RLock() 118 | defer n.mu.RUnlock() 119 | return n.node.Spec.ProviderID 120 | } 121 | 122 | func (n *Node) InstanceID() string { 123 | providerID := n.ProviderID() 124 | matches := instanceIDRegex.FindStringSubmatch(providerID) 125 | if matches == nil { 126 | return providerID 127 | } 128 | for i, name := range instanceIDRegex.SubexpNames() { 129 | if name == "InstanceID" { 130 | return matches[i] 131 | } 132 | } 133 | return providerID 134 | } 135 | 136 | func (n *Node) BindPod(pod *Pod) { 137 | n.mu.Lock() 138 | defer n.mu.Unlock() 139 | key := objectKey{ 140 | namespace: pod.Namespace(), 141 | name: pod.Name(), 142 | } 143 | _, alreadyBound := n.pods[key] 144 | n.pods[key] = pod 145 | 146 | if !alreadyBound { 147 | for rn, q := range pod.Requested() { 148 | existing := n.used[rn] 149 | existing.Add(q) 150 | n.used[rn] = existing 151 | } 152 | } 153 | } 154 | 155 | func (n *Node) DeletePod(namespace string, name string) { 156 | n.mu.Lock() 157 | defer n.mu.Unlock() 158 | key := objectKey{namespace: namespace, name: name} 159 | if p, ok := n.pods[key]; ok { 160 | // subtract the pod requests 161 | for rn, q := range p.Requested() { 162 | existing := n.used[rn] 163 | existing.Sub(q) 164 | n.used[rn] = existing 165 | } 166 | delete(n.pods, key) 167 | } 168 | } 169 | 170 | func (n *Node) Allocatable() v1.ResourceList { 171 | n.mu.RLock() 172 | defer n.mu.RUnlock() 173 | // shouldn't be modified so it's safe to return 174 | return n.node.Status.Allocatable 175 | } 176 | 177 | func (n *Node) Used() v1.ResourceList { 178 | n.mu.RLock() 179 | defer n.mu.RUnlock() 180 | used := v1.ResourceList{} 181 | for rn, q := range n.used { 182 | used[rn] = q.DeepCopy() 183 | } 184 | return used 185 | } 186 | 187 | func (n *Node) Cordoned() bool { 188 | n.mu.RLock() 189 | defer n.mu.RUnlock() 190 | if n.node.Spec.Unschedulable { 191 | return true 192 | } 193 | for _, taint := range n.node.Spec.Taints { 194 | if taint.Key == "karpenter.sh/disruption" && taint.Effect == v1.TaintEffectNoSchedule { 195 | return true 196 | } 197 | } 198 | return false 199 | } 200 | 201 | func (n *Node) Ready() bool { 202 | ready := false 203 | n.mu.RLock() 204 | for _, c := range n.node.Status.Conditions { 205 | if c.Status == v1.ConditionTrue && c.Type == v1.NodeReady { 206 | ready = true 207 | break 208 | } 209 | } 210 | n.mu.RUnlock() 211 | // when the node goes ready, remove the nodeclaim creation ts, if any 212 | if ready { 213 | n.mu.Lock() 214 | n.nodeclaimCreationTime = time.Time{} 215 | n.mu.Unlock() 216 | } 217 | return ready 218 | } 219 | 220 | func (n *Node) Created() time.Time { 221 | n.mu.RLock() 222 | defer n.mu.RUnlock() 223 | if !n.nodeclaimCreationTime.IsZero() { 224 | return n.nodeclaimCreationTime 225 | } 226 | return n.node.CreationTimestamp.Time 227 | } 228 | 229 | func (n *Node) InstanceType() ec2types.InstanceType { 230 | n.mu.RLock() 231 | defer n.mu.RUnlock() 232 | if n.IsFargate() { 233 | if len(n.Pods()) == 1 { 234 | cpu, mem, ok := n.Pods()[0].FargateCapacityProvisioned() 235 | if ok { 236 | return ec2types.InstanceType(fmt.Sprintf("%gvCPU-%gGB", cpu, mem)) 237 | } 238 | } 239 | return "Fargate" 240 | } 241 | return ec2types.InstanceType(n.node.Labels[v1.LabelInstanceTypeStable]) 242 | } 243 | 244 | func (n *Node) Zone() string { 245 | n.mu.RLock() 246 | defer n.mu.RUnlock() 247 | return n.node.Labels[v1.LabelTopologyZone] 248 | } 249 | 250 | func (n *Node) NumPods() int { 251 | n.mu.RLock() 252 | defer n.mu.RUnlock() 253 | return len(n.pods) 254 | } 255 | 256 | func (n *Node) Hide() { 257 | n.mu.Lock() 258 | defer n.mu.Unlock() 259 | n.visible = false 260 | } 261 | func (n *Node) Visible() bool { 262 | n.mu.RLock() 263 | defer n.mu.RUnlock() 264 | return n.visible 265 | } 266 | 267 | func (n *Node) Show() { 268 | n.mu.Lock() 269 | defer n.mu.Unlock() 270 | n.visible = true 271 | } 272 | 273 | func (n *Node) Deleting() bool { 274 | n.mu.Lock() 275 | defer n.mu.Unlock() 276 | return !n.node.DeletionTimestamp.IsZero() 277 | } 278 | 279 | func (n *Node) Pods() []*Pod { 280 | var pods []*Pod 281 | for _, p := range n.pods { 282 | pods = append(pods, p) 283 | } 284 | return pods 285 | } 286 | 287 | func (n *Node) HasPrice() bool { 288 | // we use NaN for an unknown price, so if this is true the price is known 289 | return n.Price == n.Price 290 | } 291 | 292 | var resourceLabelRe = regexp.MustCompile("eks-node-viewer/node-(.*?)-usage") 293 | 294 | // ComputeLabel computes dynamic labels 295 | func (n *Node) ComputeLabel(labelName string) string { 296 | switch labelName { 297 | case "eks-node-viewer/node-age": 298 | return duration.HumanDuration(time.Since(n.Created())) 299 | } 300 | // resource based custom labels 301 | if match := resourceLabelRe.FindStringSubmatch(labelName); len(match) > 0 { 302 | return pctUsage(n.Allocatable(), n.Used(), match[1]) 303 | } 304 | return "-" 305 | } 306 | 307 | // NotReadyTime is the time that the node went NotReady, or when it was created if it hasn't been marked as NotReady. 308 | func (n *Node) NotReadyTime() time.Time { 309 | n.mu.RLock() 310 | var notReadyTransitionTime time.Time 311 | for _, c := range n.node.Status.Conditions { 312 | if c.Type == v1.NodeReady && (c.Status == v1.ConditionFalse || c.Status == v1.ConditionUnknown) { 313 | notReadyTransitionTime = c.LastTransitionTime.Time 314 | break 315 | } 316 | } 317 | n.mu.RUnlock() 318 | if !notReadyTransitionTime.IsZero() { 319 | // if there's a nodeclaim creation ts, use it if the node has never been Ready before 320 | if !n.nodeclaimCreationTime.IsZero() { 321 | return n.nodeclaimCreationTime 322 | } 323 | return notReadyTransitionTime 324 | } 325 | return n.Created() 326 | } 327 | 328 | func (n *Node) SetPrice(price float64) { 329 | n.Price = price 330 | } 331 | 332 | func pctUsage(allocatable v1.ResourceList, used v1.ResourceList, resource string) string { 333 | allocRes, hasAlloc := allocatable[v1.ResourceName(resource)] 334 | if !hasAlloc { 335 | return "N/A" 336 | } 337 | usedRes, hasUsed := used[v1.ResourceName(resource)] 338 | if !hasUsed || usedRes.AsApproximateFloat64() == 0 { 339 | return "0%" 340 | } 341 | pctUsed := 0.0 342 | if allocRes.AsApproximateFloat64() != 0 { 343 | pctUsed = 100 * (usedRes.AsApproximateFloat64() / allocRes.AsApproximateFloat64()) 344 | } 345 | return fmt.Sprintf("%.0f%%", pctUsed) 346 | } 347 | -------------------------------------------------------------------------------- /pkg/aws/zz_generated_aws_cn.pricing.go: -------------------------------------------------------------------------------- 1 | //go:build !ignore_autogenerated 2 | 3 | /* 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package aws 18 | 19 | // generated at 2024-11-06T01:00:11Z for cn-north-1 20 | 21 | import ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types" 22 | 23 | var InitialOnDemandPricesCN = map[string]map[ec2types.InstanceType]float64{ 24 | "cn-north-1": { 25 | // c3 family 26 | "c3.2xlarge": 4.217000, "c3.4xlarge": 8.434000, "c3.8xlarge": 16.869000, "c3.large": 1.054000, 27 | "c3.xlarge": 2.109000, 28 | // c4 family 29 | "c4.2xlarge": 4.535000, "c4.4xlarge": 9.071000, "c4.8xlarge": 18.141000, "c4.large": 1.134000, 30 | "c4.xlarge": 2.268000, 31 | // c5 family 32 | "c5.12xlarge": 17.745000, "c5.18xlarge": 26.617000, "c5.24xlarge": 35.490000, "c5.2xlarge": 2.957000, 33 | "c5.4xlarge": 5.915000, "c5.9xlarge": 13.309000, "c5.large": 0.739000, "c5.metal": 35.490000, 34 | "c5.xlarge": 1.479000, 35 | // c5a family 36 | "c5a.12xlarge": 15.937000, "c5a.16xlarge": 21.250000, "c5a.24xlarge": 31.875000, "c5a.2xlarge": 2.656000, 37 | "c5a.4xlarge": 5.312000, "c5a.8xlarge": 10.625000, "c5a.large": 0.664000, "c5a.xlarge": 1.328000, 38 | // c5d family 39 | "c5d.12xlarge": 21.852000, "c5d.18xlarge": 32.779000, "c5d.24xlarge": 43.705000, "c5d.2xlarge": 3.642000, 40 | "c5d.4xlarge": 7.284000, "c5d.9xlarge": 16.389000, "c5d.large": 0.911000, "c5d.metal": 43.705000, 41 | "c5d.xlarge": 1.821000, 42 | // c6g family 43 | "c6g.12xlarge": 14.064400, "c6g.16xlarge": 18.752600, "c6g.2xlarge": 2.344100, "c6g.4xlarge": 4.688100, 44 | "c6g.8xlarge": 9.376300, "c6g.large": 0.586000, "c6g.medium": 0.293000, "c6g.metal": 19.390900, 45 | "c6g.xlarge": 1.172000, 46 | // c6gn family 47 | "c6gn.12xlarge": 17.869700, "c6gn.16xlarge": 23.826270, "c6gn.2xlarge": 2.978280, "c6gn.4xlarge": 5.956570, 48 | "c6gn.8xlarge": 11.913140, "c6gn.large": 0.744570, "c6gn.medium": 0.372290, "c6gn.xlarge": 1.489140, 49 | // c6i family 50 | "c6i.12xlarge": 17.744830, "c6i.16xlarge": 23.659780, "c6i.24xlarge": 35.489660, "c6i.2xlarge": 2.957470, 51 | "c6i.32xlarge": 47.319550, "c6i.4xlarge": 5.914940, "c6i.8xlarge": 11.829890, "c6i.large": 0.739370, 52 | "c6i.metal": 47.319550, "c6i.xlarge": 1.478740, 53 | // c7g family 54 | "c7g.12xlarge": 15.083100, "c7g.16xlarge": 20.110800, "c7g.2xlarge": 2.513900, "c7g.4xlarge": 5.027700, 55 | "c7g.8xlarge": 10.055400, "c7g.large": 0.628500, "c7g.medium": 0.314200, "c7g.metal": 20.110800, 56 | "c7g.xlarge": 1.256900, 57 | // d2 family 58 | "d2.2xlarge": 13.345000, "d2.4xlarge": 26.690000, "d2.8xlarge": 53.380000, "d2.xlarge": 6.673000, 59 | // g3 family 60 | "g3.16xlarge": 64.817900, "g3.4xlarge": 16.204500, "g3.8xlarge": 32.409000, 61 | // g3s family 62 | "g3s.xlarge": 11.282000, 63 | // g4dn family 64 | "g4dn.12xlarge": 38.849000, "g4dn.16xlarge": 43.218000, "g4dn.2xlarge": 7.468000, "g4dn.4xlarge": 11.956000, 65 | "g4dn.8xlarge": 21.609000, "g4dn.xlarge": 5.223000, 66 | // g5 family 67 | "g5.12xlarge": 53.640920, "g5.16xlarge": 38.736460, "g5.24xlarge": 77.018980, "g5.2xlarge": 11.462060, 68 | "g5.48xlarge": 154.037950, "g5.4xlarge": 15.358400, "g5.8xlarge": 23.151090, "g5.xlarge": 9.513890, 69 | // i2 family 70 | "i2.2xlarge": 20.407000, "i2.4xlarge": 40.815000, "i2.8xlarge": 81.630000, "i2.xlarge": 10.204000, 71 | // i3 family 72 | "i3.16xlarge": 49.948000, "i3.2xlarge": 6.244000, "i3.4xlarge": 12.487000, "i3.8xlarge": 24.974000, 73 | "i3.large": 1.561000, "i3.xlarge": 3.122000, 74 | // i3en family 75 | "i3en.12xlarge": 54.302000, "i3en.24xlarge": 108.605000, "i3en.2xlarge": 9.050000, "i3en.3xlarge": 13.576000, 76 | "i3en.6xlarge": 27.151000, "i3en.large": 2.263000, "i3en.xlarge": 4.525000, 77 | // i4i family 78 | "i4i.12xlarge": 43.665000, "i4i.16xlarge": 58.221000, "i4i.24xlarge": 87.330860, "i4i.2xlarge": 7.278000, 79 | "i4i.32xlarge": 116.441150, "i4i.4xlarge": 14.555000, "i4i.8xlarge": 29.110000, "i4i.large": 1.819000, 80 | "i4i.xlarge": 3.639000, 81 | // inf1 family 82 | "inf1.24xlarge": 47.342000, "inf1.2xlarge": 3.630000, "inf1.6xlarge": 11.835000, "inf1.xlarge": 2.288000, 83 | // m1 family 84 | "m1.small": 0.442000, 85 | // m3 family 86 | "m3.2xlarge": 6.942000, "m3.large": 1.735000, "m3.medium": 0.868000, "m3.xlarge": 3.471000, 87 | // m4 family 88 | "m4.10xlarge": 28.121000, "m4.16xlarge": 44.995000, "m4.2xlarge": 5.624000, "m4.4xlarge": 11.248000, 89 | "m4.large": 1.405000, "m4.xlarge": 2.815000, 90 | // m5 family 91 | "m5.12xlarge": 24.317000, "m5.16xlarge": 32.423000, "m5.24xlarge": 48.634000, "m5.2xlarge": 4.053000, 92 | "m5.4xlarge": 8.106000, "m5.8xlarge": 16.211000, "m5.large": 1.013000, "m5.metal": 48.634000, 93 | "m5.xlarge": 2.026000, 94 | // m5a family 95 | "m5a.12xlarge": 21.852000, "m5a.16xlarge": 29.137000, "m5a.24xlarge": 43.705000, "m5a.2xlarge": 3.642000, 96 | "m5a.4xlarge": 7.284000, "m5a.8xlarge": 14.568000, "m5a.large": 0.911000, "m5a.xlarge": 1.821000, 97 | // m5d family 98 | "m5d.12xlarge": 30.561000, "m5d.16xlarge": 40.747000, "m5d.24xlarge": 61.121000, "m5d.2xlarge": 5.093000, 99 | "m5d.4xlarge": 10.187000, "m5d.8xlarge": 20.374000, "m5d.large": 1.273000, "m5d.metal": 61.121000, 100 | "m5d.xlarge": 2.547000, 101 | // m6g family 102 | "m6g.12xlarge": 19.289300, "m6g.16xlarge": 25.719100, "m6g.2xlarge": 3.214900, "m6g.4xlarge": 6.429800, 103 | "m6g.8xlarge": 12.859500, "m6g.large": 0.803700, "m6g.medium": 0.401900, "m6g.metal": 26.486300, 104 | "m6g.xlarge": 1.607400, 105 | // m6i family 106 | "m6i.12xlarge": 24.316990, "m6i.16xlarge": 32.422660, "m6i.24xlarge": 48.633980, "m6i.2xlarge": 4.052830, 107 | "m6i.32xlarge": 64.845310, "m6i.4xlarge": 8.105660, "m6i.8xlarge": 16.211330, "m6i.large": 1.013210, 108 | "m6i.metal": 64.845310, "m6i.xlarge": 2.026420, 109 | // m7g family 110 | "m7g.12xlarge": 20.669400, "m7g.16xlarge": 27.559300, "m7g.2xlarge": 3.444900, "m7g.4xlarge": 6.889800, 111 | "m7g.8xlarge": 13.779600, "m7g.large": 0.861200, "m7g.medium": 0.430600, "m7g.metal": 27.559300, 112 | "m7g.xlarge": 1.722500, 113 | // p2 family 114 | "p2.16xlarge": 169.792000, "p2.8xlarge": 84.896000, "p2.xlarge": 10.612000, 115 | // p3 family 116 | "p3.16xlarge": 288.627000, "p3.2xlarge": 36.078000, "p3.8xlarge": 144.314000, 117 | // r3 family 118 | "r3.2xlarge": 9.803600, "r3.4xlarge": 19.607300, "r3.8xlarge": 39.214700, "r3.large": 2.450900, 119 | "r3.xlarge": 4.901800, 120 | // r4 family 121 | "r4.16xlarge": 62.746000, "r4.2xlarge": 7.842000, "r4.4xlarge": 15.683000, "r4.8xlarge": 31.373000, 122 | "r4.large": 1.959000, "r4.xlarge": 3.924000, 123 | // r5 family 124 | "r5.12xlarge": 29.246000, "r5.16xlarge": 38.995000, "r5.24xlarge": 58.492000, "r5.2xlarge": 4.874000, 125 | "r5.4xlarge": 9.749000, "r5.8xlarge": 19.497000, "r5.large": 1.219000, "r5.metal": 58.492000, 126 | "r5.xlarge": 2.437000, 127 | // r5a family 128 | "r5a.12xlarge": 26.322000, "r5a.16xlarge": 35.095000, "r5a.24xlarge": 52.643000, "r5a.2xlarge": 4.387000, 129 | "r5a.4xlarge": 8.774000, "r5a.8xlarge": 17.548000, "r5a.large": 1.097000, "r5a.xlarge": 2.193000, 130 | // r5d family 131 | "r5d.12xlarge": 35.490000, "r5d.16xlarge": 47.320000, "r5d.24xlarge": 70.979000, "r5d.2xlarge": 5.915000, 132 | "r5d.4xlarge": 11.830000, "r5d.8xlarge": 23.660000, "r5d.large": 1.479000, "r5d.metal": 70.979000, 133 | "r5d.xlarge": 2.957000, 134 | // r6g family 135 | "r6g.12xlarge": 23.199700, "r6g.16xlarge": 30.933000, "r6g.2xlarge": 3.866600, "r6g.4xlarge": 7.733200, 136 | "r6g.8xlarge": 15.466500, "r6g.large": 0.966700, "r6g.medium": 0.483300, "r6g.metal": 31.847000, 137 | "r6g.xlarge": 1.933300, 138 | // r6gd family 139 | "r6gd.12xlarge": 28.096000, "r6gd.16xlarge": 37.461300, "r6gd.2xlarge": 4.682700, "r6gd.4xlarge": 9.365300, 140 | "r6gd.8xlarge": 18.730700, "r6gd.large": 1.170700, "r6gd.medium": 0.585300, "r6gd.metal": 37.461300, 141 | "r6gd.xlarge": 2.341300, 142 | // r6i family 143 | "r6i.12xlarge": 29.246110, "r6i.16xlarge": 38.994820, "r6i.24xlarge": 58.492220, "r6i.2xlarge": 4.874350, 144 | "r6i.32xlarge": 77.989630, "r6i.4xlarge": 9.748700, "r6i.8xlarge": 19.497410, "r6i.large": 1.218590, 145 | "r6i.metal": 77.989630, "r6i.xlarge": 2.437180, 146 | // r7g family 147 | "r7g.12xlarge": 24.875600, "r7g.16xlarge": 33.167500, "r7g.2xlarge": 4.145900, "r7g.4xlarge": 8.291900, 148 | "r7g.8xlarge": 16.583800, "r7g.large": 1.036500, "r7g.medium": 0.518200, "r7g.metal": 33.167500, 149 | "r7g.xlarge": 2.073000, 150 | // t1 family 151 | "t1.micro": 0.221000, 152 | // t2 family 153 | "t2.2xlarge": 3.392000, "t2.large": 0.851000, "t2.medium": 0.426000, "t2.micro": 0.106000, 154 | "t2.nano": 0.060600, "t2.small": 0.212000, "t2.xlarge": 1.696000, 155 | // t3 family 156 | "t3.2xlarge": 2.103100, "t3.large": 0.525800, "t3.medium": 0.262900, "t3.micro": 0.065700, 157 | "t3.nano": 0.032900, "t3.small": 0.131400, "t3.xlarge": 1.051500, 158 | // t3a family 159 | "t3a.2xlarge": 1.892800, "t3a.large": 0.473200, "t3a.medium": 0.236600, "t3a.micro": 0.059100, 160 | "t3a.nano": 0.029600, "t3a.small": 0.118300, "t3a.xlarge": 0.946400, 161 | // t4g family 162 | "t4g.2xlarge": 1.621100, "t4g.large": 0.405300, "t4g.medium": 0.202600, "t4g.micro": 0.050700, 163 | "t4g.nano": 0.025300, "t4g.small": 0.101300, "t4g.xlarge": 0.810600, 164 | // u-12tb1 family 165 | "u-12tb1.112xlarge": 1074.685080, 166 | // u-6tb1 family 167 | "u-6tb1.112xlarge": 537.342540, "u-6tb1.56xlarge": 456.681190, 168 | // u-9tb1 family 169 | "u-9tb1.112xlarge": 805.979580, 170 | // x1 family 171 | "x1.16xlarge": 68.876000, "x1.32xlarge": 137.752000, 172 | // x2idn family 173 | "x2idn.16xlarge": 68.870760, "x2idn.24xlarge": 103.306140, "x2idn.32xlarge": 137.741520, 174 | "x2idn.metal": 137.741520, 175 | // x2iedn family 176 | "x2iedn.16xlarge": 137.741520, "x2iedn.24xlarge": 206.612280, "x2iedn.2xlarge": 17.217690, 177 | "x2iedn.32xlarge": 275.483040, "x2iedn.4xlarge": 34.435380, "x2iedn.8xlarge": 68.870760, 178 | "x2iedn.metal": 275.483040, "x2iedn.xlarge": 8.608850, 179 | }, 180 | } 181 | -------------------------------------------------------------------------------- /pkg/model/uimodel.go: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | 15 | package model 16 | 17 | import ( 18 | "bytes" 19 | "fmt" 20 | "io" 21 | "sort" 22 | "strings" 23 | "time" 24 | 25 | "github.com/charmbracelet/bubbles/paginator" 26 | "github.com/charmbracelet/bubbles/progress" 27 | tea "github.com/charmbracelet/bubbletea" 28 | "github.com/charmbracelet/lipgloss" 29 | "github.com/facette/natsort" 30 | "golang.org/x/text/language" 31 | "golang.org/x/text/message" 32 | v1 "k8s.io/api/core/v1" 33 | "k8s.io/apimachinery/pkg/util/duration" 34 | 35 | "github.com/awslabs/eks-node-viewer/pkg/text" 36 | ) 37 | 38 | var ( 39 | helpStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#626262")).Render 40 | // white / black 41 | activeDot = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "235", Dark: "252"}).Render("•") 42 | // black / white 43 | inactiveDot = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "250", Dark: "238"}).Render("•") 44 | ) 45 | 46 | type UIModel struct { 47 | progress progress.Model 48 | cluster *Cluster 49 | extraLabels []string 50 | paginator paginator.Model 51 | height int 52 | nodeSorter func(lhs, rhs *Node) bool 53 | style *Style 54 | DisablePricing bool 55 | } 56 | 57 | func NewUIModel(extraLabels []string, nodeSort string, style *Style) *UIModel { 58 | pager := paginator.New() 59 | pager.Type = paginator.Dots 60 | pager.ActiveDot = activeDot 61 | pager.InactiveDot = inactiveDot 62 | return &UIModel{ 63 | // red to green 64 | progress: progress.New(style.gradient), 65 | cluster: NewCluster(), 66 | extraLabels: extraLabels, 67 | paginator: pager, 68 | nodeSorter: makeNodeSorter(nodeSort), 69 | style: style, 70 | } 71 | } 72 | 73 | func (u *UIModel) Cluster() *Cluster { 74 | return u.cluster 75 | } 76 | 77 | func (u *UIModel) Init() tea.Cmd { 78 | return nil 79 | } 80 | 81 | func (u *UIModel) View() string { 82 | b := strings.Builder{} 83 | 84 | stats := u.cluster.Stats() 85 | 86 | sort.Slice(stats.Nodes, func(a, b int) bool { 87 | return u.nodeSorter(stats.Nodes[a], stats.Nodes[b]) 88 | }) 89 | 90 | ctw := text.NewColorTabWriter(&b, 0, 8, 1) 91 | u.writeClusterSummary(u.cluster.resources, stats, ctw) 92 | ctw.Flush() 93 | u.progress.ShowPercentage = true 94 | // message printer formats numbers nicely with commas 95 | enPrinter := message.NewPrinter(language.English) 96 | enPrinter.Fprintf(&b, "%d pods (%d pending %d running %d bound)\n", stats.TotalPods, 97 | stats.PodsByPhase[v1.PodPending], stats.PodsByPhase[v1.PodRunning], stats.BoundPodCount) 98 | 99 | if stats.NumNodes == 0 { 100 | fmt.Fprintln(&b) 101 | fmt.Fprintln(&b, "Waiting for update or no nodes found...") 102 | fmt.Fprintln(&b, u.paginator.View()) 103 | fmt.Fprintln(&b, helpStyle("←/→ page • q: quit")) 104 | return b.String() 105 | } 106 | 107 | fmt.Fprintln(&b) 108 | u.paginator.PerPage = u.computeItemsPerPage(stats.Nodes, &b) 109 | u.paginator.SetTotalPages(stats.NumNodes) 110 | // check if we're on a page that is outside of the NumNode upper bound 111 | if u.paginator.Page*u.paginator.PerPage > stats.NumNodes { 112 | // set the page to the last page 113 | u.paginator.Page = u.paginator.TotalPages - 1 114 | } 115 | start, end := u.paginator.GetSliceBounds(stats.NumNodes) 116 | if start >= 0 && end >= start { 117 | for _, n := range stats.Nodes[start:end] { 118 | u.writeNodeInfo(n, ctw, u.cluster.resources) 119 | } 120 | } 121 | ctw.Flush() 122 | 123 | fmt.Fprintln(&b, u.paginator.View()) 124 | fmt.Fprintln(&b, helpStyle("←/→ page • q: quit")) 125 | return b.String() 126 | } 127 | 128 | func (u *UIModel) writeNodeInfo(n *Node, w io.Writer, resources []v1.ResourceName) { 129 | allocatable := n.Allocatable() 130 | used := n.Used() 131 | firstLine := true 132 | resNameLen := 0 133 | for _, res := range resources { 134 | if len(res) > resNameLen { 135 | resNameLen = len(res) 136 | } 137 | } 138 | for _, res := range resources { 139 | usedRes := used[res] 140 | allocatableRes := allocatable[res] 141 | pct := usedRes.AsApproximateFloat64() / allocatableRes.AsApproximateFloat64() 142 | if allocatableRes.AsApproximateFloat64() == 0 { 143 | pct = 0 144 | } 145 | 146 | if firstLine { 147 | priceLabel := fmt.Sprintf("/$%0.4f", n.Price) 148 | if !n.HasPrice() || u.DisablePricing { 149 | priceLabel = "" 150 | } 151 | maxPods, _ := allocatable.Pods().AsInt64() 152 | fmt.Fprintf(w, "%s\t%s\t%s\t(%d/%d pods)\t%s%s", n.Name(), res, u.progress.ViewAs(pct), n.NumPods(), maxPods, n.InstanceType(), priceLabel) 153 | 154 | // node compute type 155 | if n.IsOnDemand() { 156 | fmt.Fprintf(w, "\tOn-Demand") 157 | } else if n.IsSpot() { 158 | fmt.Fprintf(w, "\tSpot") 159 | } else if n.IsFargate() { 160 | fmt.Fprintf(w, "\tFargate") 161 | } else { 162 | fmt.Fprintf(w, "\t-") 163 | } 164 | 165 | if n.IsAuto() { 166 | fmt.Fprintf(w, "/Auto") 167 | } 168 | 169 | // node status 170 | if n.Cordoned() && n.Deleting() { 171 | fmt.Fprintf(w, "\tCordoned/Deleting") 172 | } else if n.Deleting() { 173 | fmt.Fprintf(w, "\tDeleting") 174 | } else if n.Cordoned() { 175 | fmt.Fprintf(w, "\tCordoned") 176 | } else { 177 | fmt.Fprintf(w, "\t-") 178 | } 179 | 180 | // node readiness or time we've been waiting for it to be ready 181 | if n.Ready() { 182 | fmt.Fprintf(w, "\tReady") 183 | } else { 184 | fmt.Fprintf(w, "\tNotReady/%s", duration.HumanDuration(time.Since(n.NotReadyTime()))) 185 | } 186 | 187 | for _, label := range u.extraLabels { 188 | labelValue, ok := n.node.Labels[label] 189 | if !ok { 190 | // support computed label values 191 | labelValue = n.ComputeLabel(label) 192 | } 193 | fmt.Fprintf(w, "\t%s", labelValue) 194 | } 195 | 196 | } else { 197 | fmt.Fprintf(w, " \t%s\t%s\t\t\t\t\t", res, u.progress.ViewAs(pct)) 198 | for range u.extraLabels { 199 | fmt.Fprintf(w, "\t") 200 | } 201 | } 202 | fmt.Fprintln(w) 203 | firstLine = false 204 | } 205 | } 206 | 207 | func (u *UIModel) writeClusterSummary(resources []v1.ResourceName, stats Stats, w io.Writer) { 208 | firstLine := true 209 | 210 | for _, res := range resources { 211 | allocatable := stats.AllocatableResources[res] 212 | used := stats.UsedResources[res] 213 | pctUsed := 0.0 214 | if allocatable.AsApproximateFloat64() != 0 { 215 | pctUsed = 100 * (used.AsApproximateFloat64() / allocatable.AsApproximateFloat64()) 216 | } 217 | pctUsedStr := fmt.Sprintf("%0.1f%%", pctUsed) 218 | if pctUsed > 90 { 219 | pctUsedStr = u.style.green(pctUsedStr) 220 | } else if pctUsed > 60 { 221 | pctUsedStr = u.style.yellow(pctUsedStr) 222 | } else { 223 | pctUsedStr = u.style.red(pctUsedStr) 224 | } 225 | 226 | u.progress.ShowPercentage = false 227 | monthlyPrice := stats.TotalPrice * (365 * 24) / 12 // average hours per month 228 | // message printer formats numbers nicely with commas 229 | enPrinter := message.NewPrinter(language.English) 230 | clusterPrice := enPrinter.Sprintf("$%0.3f/hour | $%0.3f/month", stats.TotalPrice, monthlyPrice) 231 | if u.DisablePricing { 232 | clusterPrice = "" 233 | } 234 | if firstLine { 235 | enPrinter.Fprintf(w, "%d nodes\t(%10s/%s)\t%s\t%s\t%s\t%s\n", 236 | stats.NumNodes, used.String(), allocatable.String(), pctUsedStr, res, u.progress.ViewAs(pctUsed/100.0), clusterPrice) 237 | } else { 238 | enPrinter.Fprintf(w, " \t%s/%s\t%s\t%s\t%s\t\n", 239 | used.String(), allocatable.String(), pctUsedStr, res, u.progress.ViewAs(pctUsed/100.0)) 240 | } 241 | firstLine = false 242 | } 243 | } 244 | 245 | // computeItemsPerPage dynamically calculates the number of lines we can fit per page 246 | // taking into account header and footer text 247 | func (u *UIModel) computeItemsPerPage(nodes []*Node, b *strings.Builder) int { 248 | var buf bytes.Buffer 249 | u.writeNodeInfo(nodes[0], &buf, u.cluster.resources) 250 | headerLines := strings.Count(b.String(), "\n") + 2 251 | nodeLines := strings.Count(buf.String(), "\n") 252 | if nodeLines == 0 { 253 | nodeLines = 1 254 | } 255 | return ((u.height - headerLines) / nodeLines) - 1 256 | } 257 | 258 | type tickMsg time.Time 259 | 260 | func tickCmd() tea.Cmd { 261 | return tea.Tick(100*time.Millisecond, func(t time.Time) tea.Msg { 262 | return tickMsg(t) 263 | }) 264 | } 265 | 266 | func (u *UIModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 267 | switch msg := msg.(type) { 268 | case tea.WindowSizeMsg: 269 | u.height = msg.Height 270 | return u, tickCmd() 271 | case tea.KeyMsg: 272 | switch msg.String() { 273 | case "q", "esc", "ctrl+c": 274 | return u, tea.Quit 275 | } 276 | case tickMsg: 277 | return u, tickCmd() 278 | } 279 | var cmd tea.Cmd 280 | u.paginator, cmd = u.paginator.Update(msg) 281 | return u, cmd 282 | } 283 | 284 | func (u *UIModel) SetResources(resources []string) { 285 | u.cluster.resources = nil 286 | for _, r := range resources { 287 | u.cluster.resources = append(u.cluster.resources, v1.ResourceName(r)) 288 | } 289 | } 290 | 291 | func makeNodeSorter(nodeSort string) func(lhs *Node, rhs *Node) bool { 292 | sortOrder := func(b bool) bool { return b } 293 | if strings.HasSuffix(nodeSort, "=asc") { 294 | nodeSort = nodeSort[:len(nodeSort)-4] 295 | } 296 | if strings.HasSuffix(nodeSort, "=dsc") { 297 | sortOrder = func(b bool) bool { return !b } 298 | nodeSort = nodeSort[:len(nodeSort)-4] 299 | } 300 | 301 | if nodeSort == "creation" { 302 | return func(lhs *Node, rhs *Node) bool { 303 | if lhs.Created() == rhs.Created() { 304 | return sortOrder(natsort.Compare(lhs.Name(), rhs.Name())) 305 | } 306 | return sortOrder(rhs.Created().Before(lhs.Created())) 307 | } 308 | } 309 | 310 | return func(lhs *Node, rhs *Node) bool { 311 | lhsLabel, ok := lhs.node.Labels[nodeSort] 312 | if !ok { 313 | lhsLabel = lhs.ComputeLabel(nodeSort) 314 | } 315 | rhsLabel, ok := rhs.node.Labels[nodeSort] 316 | if !ok { 317 | rhsLabel = rhs.ComputeLabel(nodeSort) 318 | } 319 | if lhsLabel == rhsLabel { 320 | return sortOrder(natsort.Compare(lhs.InstanceID(), rhs.InstanceID())) 321 | } 322 | return sortOrder(natsort.Compare(lhsLabel, rhsLabel)) 323 | } 324 | } 325 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | -------------------------------------------------------------------------------- /pkg/aws/pricing.go: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | package aws 15 | 16 | import ( 17 | "context" 18 | "encoding/json" 19 | "errors" 20 | "log" 21 | "math" 22 | "os" 23 | "strconv" 24 | "strings" 25 | "sync" 26 | "time" 27 | 28 | "github.com/aws/aws-sdk-go-v2/aws" 29 | "github.com/aws/aws-sdk-go-v2/config" 30 | "github.com/aws/aws-sdk-go-v2/service/ec2" 31 | ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types" 32 | "github.com/aws/aws-sdk-go-v2/service/pricing" 33 | pricingtypes "github.com/aws/aws-sdk-go-v2/service/pricing/types" 34 | "go.uber.org/multierr" 35 | 36 | "github.com/awslabs/eks-node-viewer/pkg/model" 37 | nvp "github.com/awslabs/eks-node-viewer/pkg/pricing" 38 | ) 39 | 40 | type pricingProvider struct { 41 | ec2Client *ec2.Client 42 | pricingClient *pricing.Client 43 | region string 44 | 45 | mu sync.RWMutex 46 | onUpdateFuncs []func() 47 | onDemandPrices map[ec2types.InstanceType]float64 48 | autoManagementPrices map[ec2types.InstanceType]float64 49 | spotPrices map[ec2types.InstanceType]zonalPricing 50 | fargateVCPUPricePerHour float64 51 | fargateGBPricePerHour float64 52 | } 53 | 54 | func (p *pricingProvider) OnUpdate(onUpdate func()) { 55 | p.onUpdateFuncs = append(p.onUpdateFuncs, onUpdate) 56 | } 57 | 58 | func (p *pricingProvider) NodePrice(n *model.Node) (float64, bool) { 59 | autoManagementPrice := 0.0 60 | if n.IsAuto() { 61 | if price, ok := p.AutoManagementPrice(n.InstanceType()); ok { 62 | autoManagementPrice = price 63 | } else { 64 | // don't return a price until we've looked up the management price 65 | return 0.0, false 66 | } 67 | } 68 | 69 | if n.IsOnDemand() { 70 | if price, ok := p.OnDemandPrice(n.InstanceType()); ok { 71 | return autoManagementPrice + price, true 72 | } 73 | } else if n.IsSpot() { 74 | if price, ok := p.SpotPrice(n.InstanceType(), n.Zone()); ok { 75 | return autoManagementPrice + price, true 76 | } 77 | } else if n.IsFargate() && len(n.Pods()) == 1 { 78 | cpu, mem, ok := n.Pods()[0].FargateCapacityProvisioned() 79 | if ok { 80 | if price, ok := p.FargatePrice(cpu, mem); ok { 81 | return price, true 82 | } 83 | } 84 | } 85 | return math.NaN(), false 86 | } 87 | 88 | // zonalPricing is used to capture the per-zone price 89 | // for spot data as well as the default price 90 | // based on on-demand price when the controller first 91 | // comes up 92 | type zonalPricing struct { 93 | defaultPrice float64 // Used until we get the spot pricing data 94 | prices map[string]float64 95 | } 96 | 97 | func newZonalPricing(defaultPrice float64) zonalPricing { 98 | z := zonalPricing{ 99 | prices: map[string]float64{}, 100 | } 101 | z.defaultPrice = defaultPrice 102 | return z 103 | } 104 | 105 | // pricingUpdatePeriod is how often we try to update our pricing information after the initial update on startup 106 | const pricingUpdatePeriod = 12 * time.Hour 107 | 108 | // NewPricingClient returns a pricing client configured based on a particular region 109 | func NewPricingClient(ctx context.Context, region string) (*pricing.Client, error) { 110 | // pricing API doesn't have an endpoint in all regions 111 | pricingAPIRegion := "us-east-1" 112 | if strings.HasPrefix(region, "ap-") { 113 | pricingAPIRegion = "ap-south-1" 114 | } else if strings.HasPrefix(region, "cn-") { 115 | pricingAPIRegion = "cn-northwest-1" 116 | } else if strings.HasPrefix(region, "eu-") { 117 | pricingAPIRegion = "eu-central-1" 118 | } 119 | 120 | cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(pricingAPIRegion)) 121 | if err != nil { 122 | return nil, err 123 | } 124 | return pricing.NewFromConfig(cfg), nil 125 | } 126 | 127 | var allPrices = []map[string]map[ec2types.InstanceType]float64{ 128 | InitialOnDemandPricesAWS, 129 | InitialOnDemandPricesUSGov, 130 | InitialOnDemandPricesCN, 131 | } 132 | 133 | func getStaticPrices(region string) map[ec2types.InstanceType]float64 { 134 | for _, priceSet := range allPrices { 135 | if prices, ok := priceSet[region]; ok { 136 | return prices 137 | } 138 | } 139 | return InitialOnDemandPricesAWS["us-east-1"] 140 | } 141 | 142 | func NewStaticPricingProvider() nvp.Provider { 143 | region := os.Getenv("AWS_REGION") 144 | if region == "" { 145 | region = "us-east-1" 146 | } 147 | 148 | return &pricingProvider{ 149 | onDemandPrices: getStaticPrices(region), 150 | spotPrices: map[ec2types.InstanceType]zonalPricing{}, 151 | } 152 | } 153 | 154 | func NewPricingProvider(ctx context.Context, cfg aws.Config) nvp.Provider { 155 | region := cfg.Region 156 | if region == "" { 157 | region = "us-west-2" 158 | } 159 | 160 | ec2Client := ec2.NewFromConfig(cfg) 161 | pricingClient, err := NewPricingClient(ctx, region) 162 | if err != nil { 163 | log.Printf("Failed to create pricing client: %v", err) 164 | pricingClient = nil 165 | } 166 | 167 | p := &pricingProvider{ 168 | region: region, 169 | onDemandPrices: getStaticPrices(region), 170 | spotPrices: map[ec2types.InstanceType]zonalPricing{}, 171 | ec2Client: ec2Client, 172 | pricingClient: pricingClient, 173 | } 174 | 175 | go func() { 176 | // perform an initial price update at startup 177 | p.updatePricing(ctx) 178 | 179 | for { 180 | select { 181 | case <-ctx.Done(): 182 | return 183 | case <-time.After(pricingUpdatePeriod): 184 | p.updatePricing(ctx) 185 | } 186 | } 187 | }() 188 | return p 189 | } 190 | 191 | // OnDemandPrice returns the last known on-demand price for a given instance type, returning an error if there is no 192 | // known on-demand pricing for the instance type. 193 | func (p *pricingProvider) OnDemandPrice(instanceType ec2types.InstanceType) (float64, bool) { 194 | p.mu.RLock() 195 | defer p.mu.RUnlock() 196 | price, ok := p.onDemandPrices[instanceType] 197 | if !ok { 198 | return 0.0, false 199 | } 200 | return price, true 201 | } 202 | 203 | // AutoManagementPrice returns the EKS Auto Mode management price for the given instance type. 204 | func (p *pricingProvider) AutoManagementPrice(instanceType ec2types.InstanceType) (float64, bool) { 205 | p.mu.RLock() 206 | defer p.mu.RUnlock() 207 | price, ok := p.autoManagementPrices[instanceType] 208 | if !ok { 209 | return 0.0, false 210 | } 211 | return price, true 212 | } 213 | 214 | // FargatePrice returns the last known Fargate price for the given CPU/memory. 215 | func (p *pricingProvider) FargatePrice(cpu, memory float64) (float64, bool) { 216 | p.mu.RLock() 217 | defer p.mu.RUnlock() 218 | if p.fargateGBPricePerHour == 0 || p.fargateVCPUPricePerHour == 0 { 219 | return 0, false 220 | } 221 | return cpu*p.fargateVCPUPricePerHour + memory*p.fargateGBPricePerHour, true 222 | } 223 | 224 | // SpotPrice returns the last known spot price for a given instance type and zone, returning an error 225 | // if there is no known spot pricing for that instance type or zone 226 | func (p *pricingProvider) SpotPrice(instanceType ec2types.InstanceType, zone string) (float64, bool) { 227 | p.mu.RLock() 228 | defer p.mu.RUnlock() 229 | if val, ok := p.spotPrices[instanceType]; ok { 230 | if price, ok := p.spotPrices[instanceType].prices[zone]; ok { 231 | return price, true 232 | } 233 | return val.defaultPrice, true 234 | } 235 | return 0.0, false 236 | } 237 | 238 | func (p *pricingProvider) updatePricing(ctx context.Context) { 239 | var wg sync.WaitGroup 240 | wg.Add(1) 241 | go func() { 242 | defer wg.Done() 243 | if err := p.updateOnDemandPricing(ctx); err != nil { 244 | log.Printf("updating on-demand pricing, %s, using existing pricing data", err) 245 | } 246 | }() 247 | 248 | wg.Add(1) 249 | go func() { 250 | defer wg.Done() 251 | if err := p.updateSpotPricing(ctx); err != nil { 252 | log.Printf("updating spot pricing, %s, using existing pricing data", err) 253 | } 254 | }() 255 | 256 | wg.Add(1) 257 | go func() { 258 | defer wg.Done() 259 | if err := p.updateFargatePricing(ctx); err != nil { 260 | log.Printf("updating fargate pricing, %s", err) 261 | } 262 | }() 263 | 264 | wg.Add(1) 265 | go func() { 266 | defer wg.Done() 267 | if err := p.updateAutoManagementPricing(ctx); err != nil { 268 | log.Printf("updating auto management pricing, %s", err) 269 | } 270 | }() 271 | wg.Wait() 272 | 273 | // notify anyone that cares 274 | for _, f := range p.onUpdateFuncs { 275 | f() 276 | } 277 | } 278 | 279 | func (p *pricingProvider) updateAutoManagementPricing(ctx context.Context) error { 280 | if p.pricingClient == nil { 281 | return errors.New("pricing client not initialized") 282 | } 283 | prices := map[ec2types.InstanceType]float64{} 284 | filters := []pricingtypes.Filter{ 285 | { 286 | Type: pricingtypes.FilterTypeTermMatch, 287 | Field: aws.String("operation"), 288 | Value: aws.String("EKSAutoUsage"), 289 | }, 290 | { 291 | Type: pricingtypes.FilterTypeTermMatch, 292 | Field: aws.String("regionCode"), 293 | Value: aws.String("us-west-2"), 294 | }, 295 | } 296 | 297 | paginator := pricing.NewGetProductsPaginator(p.pricingClient, &pricing.GetProductsInput{ 298 | Filters: filters, 299 | ServiceCode: aws.String("AmazonEKS"), 300 | MaxResults: aws.Int32(100), 301 | }) 302 | 303 | for paginator.HasMorePages() { 304 | output, err := paginator.NextPage(ctx) 305 | if err != nil { 306 | return err 307 | } 308 | p.processInstancePricingPage(output, prices) 309 | } 310 | if len(prices) == 0 { 311 | log.Printf("No Auto Mode managment prices found") 312 | } 313 | 314 | p.mu.Lock() 315 | defer p.mu.Unlock() 316 | p.autoManagementPrices = prices 317 | return nil 318 | } 319 | 320 | func (p *pricingProvider) updateOnDemandPricing(ctx context.Context) error { 321 | if p.pricingClient == nil { 322 | return errors.New("pricing client not initialized") 323 | } 324 | 325 | // standard on-demand instances 326 | var wg sync.WaitGroup 327 | var onDemandPrices, onDemandMetalPrices map[ec2types.InstanceType]float64 328 | var onDemandErr, onDemandMetalErr error 329 | 330 | wg.Add(1) 331 | go func() { 332 | defer wg.Done() 333 | onDemandPrices, onDemandErr = p.fetchOnDemandPricing(ctx, 334 | pricingtypes.Filter{ 335 | Field: aws.String("tenancy"), 336 | Type: pricingtypes.FilterTypeTermMatch, 337 | Value: aws.String("Shared"), 338 | }, 339 | pricingtypes.Filter{ 340 | Field: aws.String("productFamily"), 341 | Type: pricingtypes.FilterTypeTermMatch, 342 | Value: aws.String("Compute Instance"), 343 | }) 344 | }() 345 | 346 | // bare metal on-demand prices 347 | wg.Add(1) 348 | go func() { 349 | defer wg.Done() 350 | onDemandMetalPrices, onDemandMetalErr = p.fetchOnDemandPricing(ctx, 351 | pricingtypes.Filter{ 352 | Field: aws.String("tenancy"), 353 | Type: pricingtypes.FilterTypeTermMatch, 354 | Value: aws.String("Dedicated"), 355 | }, 356 | pricingtypes.Filter{ 357 | Field: aws.String("productFamily"), 358 | Type: pricingtypes.FilterTypeTermMatch, 359 | Value: aws.String("Compute Instance (bare metal)"), 360 | }) 361 | }() 362 | 363 | wg.Wait() 364 | err := multierr.Append(onDemandErr, onDemandMetalErr) 365 | if err != nil { 366 | return err 367 | } 368 | 369 | if len(onDemandPrices) == 0 || len(onDemandMetalPrices) == 0 { 370 | return errors.New("no on-demand pricing found") 371 | } 372 | p.mu.Lock() 373 | defer p.mu.Unlock() 374 | 375 | p.onDemandPrices = map[ec2types.InstanceType]float64{} 376 | for _, m := range []map[ec2types.InstanceType]float64{onDemandPrices, onDemandMetalPrices} { 377 | for k, v := range m { 378 | p.onDemandPrices[k] = v 379 | } 380 | } 381 | return nil 382 | } 383 | 384 | func (p *pricingProvider) fetchOnDemandPricing(ctx context.Context, additionalFilters ...pricingtypes.Filter) (map[ec2types.InstanceType]float64, error) { 385 | prices := map[ec2types.InstanceType]float64{} 386 | filters := append([]pricingtypes.Filter{ 387 | { 388 | Field: aws.String("regionCode"), 389 | Type: pricingtypes.FilterTypeTermMatch, 390 | Value: aws.String(p.region), 391 | }, 392 | { 393 | Field: aws.String("serviceCode"), 394 | Type: pricingtypes.FilterTypeTermMatch, 395 | Value: aws.String("AmazonEC2"), 396 | }, 397 | { 398 | Field: aws.String("preInstalledSw"), 399 | Type: pricingtypes.FilterTypeTermMatch, 400 | Value: aws.String("NA"), 401 | }, 402 | { 403 | Field: aws.String("operatingSystem"), 404 | Type: pricingtypes.FilterTypeTermMatch, 405 | Value: aws.String("Linux"), 406 | }, 407 | { 408 | Field: aws.String("capacitystatus"), 409 | Type: pricingtypes.FilterTypeTermMatch, 410 | Value: aws.String("Used"), 411 | }, 412 | { 413 | Field: aws.String("marketoption"), 414 | Type: pricingtypes.FilterTypeTermMatch, 415 | Value: aws.String("OnDemand"), 416 | }}, 417 | additionalFilters...) 418 | 419 | paginator := pricing.NewGetProductsPaginator(p.pricingClient, &pricing.GetProductsInput{ 420 | Filters: filters, 421 | ServiceCode: aws.String("AmazonEC2"), 422 | MaxResults: aws.Int32(100), 423 | }) 424 | 425 | for paginator.HasMorePages() { 426 | output, err := paginator.NextPage(ctx) 427 | if err != nil { 428 | return nil, err 429 | } 430 | p.processInstancePricingPage(output, prices) 431 | } 432 | 433 | return prices, nil 434 | } 435 | 436 | // turning off cyclo here, it measures as a 12 due to all of the type checks of the pricing data which returns a deeply 437 | // nested map[string]interface{} 438 | // nolint: gocyclo 439 | func (p *pricingProvider) processInstancePricingPage(output *pricing.GetProductsOutput, prices map[ec2types.InstanceType]float64) { 440 | // this isn't the full pricing struct, just the portions we care about 441 | type priceItem struct { 442 | Product struct { 443 | Attributes struct { 444 | InstanceType string 445 | } 446 | } 447 | Terms struct { 448 | OnDemand map[string]struct { 449 | PriceDimensions map[string]struct { 450 | PricePerUnit map[string]string 451 | } 452 | } 453 | } 454 | } 455 | 456 | currency := "USD" 457 | if strings.HasPrefix(p.region, "cn-") { 458 | currency = "CNY" 459 | } 460 | for _, outer := range output.PriceList { 461 | dec := json.NewDecoder(strings.NewReader(outer)) 462 | var pItem priceItem 463 | if err := dec.Decode(&pItem); err != nil { 464 | log.Printf("decoding %q, %s", outer, err) 465 | } 466 | if pItem.Product.Attributes.InstanceType == "" { 467 | continue 468 | } 469 | for _, term := range pItem.Terms.OnDemand { 470 | for _, v := range term.PriceDimensions { 471 | price, err := strconv.ParseFloat(v.PricePerUnit[currency], 64) 472 | if err != nil || price == 0 { 473 | continue 474 | } 475 | prices[ec2types.InstanceType(pItem.Product.Attributes.InstanceType)] = price 476 | } 477 | } 478 | } 479 | } 480 | 481 | // nolint: gocyclo 482 | func (p *pricingProvider) updateSpotPricing(ctx context.Context) error { 483 | if p.ec2Client == nil { 484 | return errors.New("ec2 client not initialized") 485 | } 486 | 487 | prices := map[ec2types.InstanceType]map[string]float64{} 488 | 489 | paginator := ec2.NewDescribeSpotPriceHistoryPaginator(p.ec2Client, &ec2.DescribeSpotPriceHistoryInput{ 490 | ProductDescriptions: []string{"Linux/UNIX", "Linux/UNIX (Amazon VPC)"}, 491 | // get the latest spot price for each instance type 492 | StartTime: aws.Time(time.Now()), 493 | }) 494 | 495 | for paginator.HasMorePages() { 496 | output, err := paginator.NextPage(ctx) 497 | if err != nil { 498 | return err 499 | } 500 | 501 | for _, sph := range output.SpotPriceHistory { 502 | spotPriceStr := aws.ToString(sph.SpotPrice) 503 | spotPrice, err := strconv.ParseFloat(spotPriceStr, 64) 504 | // these errors shouldn't occur, but if pricing API does have an error, we ignore the record 505 | if err != nil { 506 | log.Printf("unable to parse price record %#v", sph) 507 | continue 508 | } 509 | if sph.Timestamp.IsZero() { 510 | continue 511 | } 512 | instanceType := sph.InstanceType 513 | az := aws.ToString(sph.AvailabilityZone) 514 | _, ok := prices[instanceType] 515 | if !ok { 516 | prices[instanceType] = map[string]float64{} 517 | } 518 | prices[instanceType][az] = spotPrice 519 | } 520 | } 521 | 522 | if len(prices) == 0 { 523 | return errors.New("no spot pricing found") 524 | } 525 | 526 | p.mu.Lock() 527 | defer p.mu.Unlock() 528 | 529 | totalOfferings := 0 530 | for it, zoneData := range prices { 531 | if _, ok := p.spotPrices[it]; !ok { 532 | p.spotPrices[it] = newZonalPricing(0) 533 | } 534 | for zone, price := range zoneData { 535 | p.spotPrices[it].prices[zone] = price 536 | } 537 | totalOfferings += len(zoneData) 538 | } 539 | return nil 540 | } 541 | 542 | func (p *pricingProvider) updateFargatePricing(ctx context.Context) error { 543 | if p.pricingClient == nil { 544 | return errors.New("pricing client not initialized") 545 | } 546 | 547 | filters := []pricingtypes.Filter{ 548 | { 549 | Field: aws.String("regionCode"), 550 | Type: pricingtypes.FilterTypeTermMatch, 551 | Value: aws.String(p.region), 552 | }, 553 | } 554 | 555 | paginator := pricing.NewGetProductsPaginator(p.pricingClient, &pricing.GetProductsInput{ 556 | Filters: filters, 557 | ServiceCode: aws.String("AmazonEKS"), 558 | MaxResults: aws.Int32(100), 559 | }) 560 | 561 | for paginator.HasMorePages() { 562 | output, err := paginator.NextPage(ctx) 563 | if err != nil { 564 | return err 565 | } 566 | p.processFargatePage(output) 567 | } 568 | 569 | return nil 570 | } 571 | 572 | func (p *pricingProvider) processFargatePage(output *pricing.GetProductsOutput) { 573 | // this isn't the full pricing struct, just the portions we care about 574 | type priceItem struct { 575 | Product struct { 576 | ProductFamily string 577 | Attributes struct { 578 | UsageType string 579 | MemoryType string 580 | } 581 | } 582 | Terms struct { 583 | OnDemand map[string]struct { 584 | PriceDimensions map[string]struct { 585 | PricePerUnit struct { 586 | USD string 587 | } 588 | } 589 | } 590 | } 591 | } 592 | 593 | for _, outer := range output.PriceList { 594 | dec := json.NewDecoder(strings.NewReader(outer)) 595 | var pItem priceItem 596 | if err := dec.Decode(&pItem); err != nil { 597 | log.Printf("decoding %s", err) 598 | } 599 | if !strings.Contains(pItem.Product.Attributes.UsageType, "Fargate") { 600 | continue 601 | } 602 | name := pItem.Product.Attributes.UsageType 603 | for _, term := range pItem.Terms.OnDemand { 604 | for _, v := range term.PriceDimensions { 605 | price, err := strconv.ParseFloat(v.PricePerUnit.USD, 64) 606 | if err != nil || price == 0 { 607 | continue 608 | } 609 | if strings.Contains(name, "vCPU-Hours") { 610 | p.mu.Lock() 611 | p.fargateVCPUPricePerHour = price 612 | p.mu.Unlock() 613 | } else if strings.Contains(name, "GB-Hours") { 614 | p.mu.Lock() 615 | p.fargateGBPricePerHour = price 616 | p.mu.Unlock() 617 | } else { 618 | log.Println("unsupported fargate price information found", name) 619 | } 620 | } 621 | } 622 | } 623 | } 624 | -------------------------------------------------------------------------------- /pkg/aws/zz_generated_aws.pricing.go: -------------------------------------------------------------------------------- 1 | //go:build !ignore_autogenerated 2 | 3 | /* 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package aws 18 | 19 | // generated at 2025-04-14T13:15:19Z for us-east-1 20 | 21 | import ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types" 22 | 23 | var InitialOnDemandPricesAWS = map[string]map[ec2types.InstanceType]float64{ 24 | // us-east-1 25 | "us-east-1": { 26 | // a1 family 27 | "a1.2xlarge": 0.204000, "a1.4xlarge": 0.408000, "a1.large": 0.051000, "a1.medium": 0.025500, 28 | "a1.metal": 0.408000, "a1.xlarge": 0.102000, 29 | // c1 family 30 | "c1.medium": 0.130000, "c1.xlarge": 0.520000, 31 | // c3 family 32 | "c3.2xlarge": 0.420000, "c3.4xlarge": 0.840000, "c3.8xlarge": 1.680000, "c3.large": 0.105000, 33 | "c3.xlarge": 0.210000, 34 | // c4 family 35 | "c4.2xlarge": 0.398000, "c4.4xlarge": 0.796000, "c4.8xlarge": 1.591000, "c4.large": 0.100000, 36 | "c4.xlarge": 0.199000, 37 | // c5 family 38 | "c5.12xlarge": 2.040000, "c5.18xlarge": 3.060000, "c5.24xlarge": 4.080000, "c5.2xlarge": 0.340000, 39 | "c5.4xlarge": 0.680000, "c5.9xlarge": 1.530000, "c5.large": 0.085000, "c5.metal": 4.080000, 40 | "c5.xlarge": 0.170000, 41 | // c5a family 42 | "c5a.12xlarge": 1.848000, "c5a.16xlarge": 2.464000, "c5a.24xlarge": 3.696000, "c5a.2xlarge": 0.308000, 43 | "c5a.4xlarge": 0.616000, "c5a.8xlarge": 1.232000, "c5a.large": 0.077000, "c5a.xlarge": 0.154000, 44 | // c5ad family 45 | "c5ad.12xlarge": 2.064000, "c5ad.16xlarge": 2.752000, "c5ad.24xlarge": 4.128000, "c5ad.2xlarge": 0.344000, 46 | "c5ad.4xlarge": 0.688000, "c5ad.8xlarge": 1.376000, "c5ad.large": 0.086000, "c5ad.xlarge": 0.172000, 47 | // c5d family 48 | "c5d.12xlarge": 2.304000, "c5d.18xlarge": 3.456000, "c5d.24xlarge": 4.608000, "c5d.2xlarge": 0.384000, 49 | "c5d.4xlarge": 0.768000, "c5d.9xlarge": 1.728000, "c5d.large": 0.096000, "c5d.metal": 4.608000, 50 | "c5d.xlarge": 0.192000, 51 | // c5n family 52 | "c5n.18xlarge": 3.888000, "c5n.2xlarge": 0.432000, "c5n.4xlarge": 0.864000, "c5n.9xlarge": 1.944000, 53 | "c5n.large": 0.108000, "c5n.metal": 3.888000, "c5n.xlarge": 0.216000, 54 | // c6a family 55 | "c6a.12xlarge": 1.836000, "c6a.16xlarge": 2.448000, "c6a.24xlarge": 3.672000, "c6a.2xlarge": 0.306000, 56 | "c6a.32xlarge": 4.896000, "c6a.48xlarge": 7.344000, "c6a.4xlarge": 0.612000, "c6a.8xlarge": 1.224000, 57 | "c6a.large": 0.076500, "c6a.metal": 7.344000, "c6a.xlarge": 0.153000, 58 | // c6g family 59 | "c6g.12xlarge": 1.632000, "c6g.16xlarge": 2.176000, "c6g.2xlarge": 0.272000, "c6g.4xlarge": 0.544000, 60 | "c6g.8xlarge": 1.088000, "c6g.large": 0.068000, "c6g.medium": 0.034000, "c6g.metal": 2.306600, 61 | "c6g.xlarge": 0.136000, 62 | // c6gd family 63 | "c6gd.12xlarge": 1.843200, "c6gd.16xlarge": 2.457600, "c6gd.2xlarge": 0.307200, "c6gd.4xlarge": 0.614400, 64 | "c6gd.8xlarge": 1.228800, "c6gd.large": 0.076800, "c6gd.medium": 0.038400, "c6gd.metal": 2.605100, 65 | "c6gd.xlarge": 0.153600, 66 | // c6gn family 67 | "c6gn.12xlarge": 2.073600, "c6gn.16xlarge": 2.764800, "c6gn.2xlarge": 0.345600, "c6gn.4xlarge": 0.691200, 68 | "c6gn.8xlarge": 1.382400, "c6gn.large": 0.086400, "c6gn.medium": 0.043200, "c6gn.xlarge": 0.172800, 69 | // c6i family 70 | "c6i.12xlarge": 2.040000, "c6i.16xlarge": 2.720000, "c6i.24xlarge": 4.080000, "c6i.2xlarge": 0.340000, 71 | "c6i.32xlarge": 5.440000, "c6i.4xlarge": 0.680000, "c6i.8xlarge": 1.360000, "c6i.large": 0.085000, 72 | "c6i.metal": 5.440000, "c6i.xlarge": 0.170000, 73 | // c6id family 74 | "c6id.12xlarge": 2.419200, "c6id.16xlarge": 3.225600, "c6id.24xlarge": 4.838400, "c6id.2xlarge": 0.403200, 75 | "c6id.32xlarge": 6.451200, "c6id.4xlarge": 0.806400, "c6id.8xlarge": 1.612800, "c6id.large": 0.100800, 76 | "c6id.metal": 6.451200, "c6id.xlarge": 0.201600, 77 | // c6in family 78 | "c6in.12xlarge": 2.721600, "c6in.16xlarge": 3.628800, "c6in.24xlarge": 5.443200, "c6in.2xlarge": 0.453600, 79 | "c6in.32xlarge": 7.257600, "c6in.4xlarge": 0.907200, "c6in.8xlarge": 1.814400, "c6in.large": 0.113400, 80 | "c6in.metal": 7.257600, "c6in.xlarge": 0.226800, 81 | // c7a family 82 | "c7a.12xlarge": 2.463360, "c7a.16xlarge": 3.284480, "c7a.24xlarge": 4.926720, "c7a.2xlarge": 0.410560, 83 | "c7a.32xlarge": 6.568960, "c7a.48xlarge": 9.853440, "c7a.4xlarge": 0.821120, "c7a.8xlarge": 1.642240, 84 | "c7a.large": 0.102640, "c7a.medium": 0.051320, "c7a.metal-48xl": 9.853440, "c7a.xlarge": 0.205280, 85 | // c7g family 86 | "c7g.12xlarge": 1.740000, "c7g.16xlarge": 2.320000, "c7g.2xlarge": 0.290000, "c7g.4xlarge": 0.580000, 87 | "c7g.8xlarge": 1.160000, "c7g.large": 0.072500, "c7g.medium": 0.036300, "c7g.metal": 2.320000, 88 | "c7g.xlarge": 0.145000, 89 | // c7gd family 90 | "c7gd.12xlarge": 2.177300, "c7gd.16xlarge": 2.903000, "c7gd.2xlarge": 0.362900, "c7gd.4xlarge": 0.725800, 91 | "c7gd.8xlarge": 1.451500, "c7gd.large": 0.090700, "c7gd.medium": 0.045400, "c7gd.metal": 2.903000, 92 | "c7gd.xlarge": 0.181400, 93 | // c7gn family 94 | "c7gn.12xlarge": 2.995200, "c7gn.16xlarge": 3.993600, "c7gn.2xlarge": 0.499200, "c7gn.4xlarge": 0.998400, 95 | "c7gn.8xlarge": 1.996800, "c7gn.large": 0.124800, "c7gn.medium": 0.062400, "c7gn.metal": 3.993600, 96 | "c7gn.xlarge": 0.249600, 97 | // c7i-flex family 98 | "c7i-flex.12xlarge": 2.034900, "c7i-flex.16xlarge": 2.713200, "c7i-flex.2xlarge": 0.339150, 99 | "c7i-flex.4xlarge": 0.678300, "c7i-flex.8xlarge": 1.356600, "c7i-flex.large": 0.084790, 100 | "c7i-flex.xlarge": 0.169580, 101 | // c7i family 102 | "c7i.12xlarge": 2.142000, "c7i.16xlarge": 2.856000, "c7i.24xlarge": 4.284000, "c7i.2xlarge": 0.357000, 103 | "c7i.48xlarge": 8.568000, "c7i.4xlarge": 0.714000, "c7i.8xlarge": 1.428000, "c7i.large": 0.089250, 104 | "c7i.metal-24xl": 4.712400, "c7i.metal-48xl": 8.568000, "c7i.xlarge": 0.178500, 105 | // c8g family 106 | "c8g.12xlarge": 1.914240, "c8g.16xlarge": 2.552320, "c8g.24xlarge": 3.828480, "c8g.2xlarge": 0.319040, 107 | "c8g.48xlarge": 7.656960, "c8g.4xlarge": 0.638080, "c8g.8xlarge": 1.276160, "c8g.large": 0.079760, 108 | "c8g.medium": 0.039880, "c8g.metal-24xl": 4.211330, "c8g.metal-48xl": 7.656960, "c8g.xlarge": 0.159520, 109 | // cr1 family 110 | "cr1.8xlarge": 3.500000, 111 | // d2 family 112 | "d2.2xlarge": 1.380000, "d2.4xlarge": 2.760000, "d2.8xlarge": 5.520000, "d2.xlarge": 0.690000, 113 | // d3 family 114 | "d3.2xlarge": 0.999000, "d3.4xlarge": 1.998000, "d3.8xlarge": 3.995520, "d3.xlarge": 0.499000, 115 | // d3en family 116 | "d3en.12xlarge": 6.308640, "d3en.2xlarge": 1.051000, "d3en.4xlarge": 2.103000, "d3en.6xlarge": 3.154000, 117 | "d3en.8xlarge": 4.205760, "d3en.xlarge": 0.526000, 118 | // dl1 family 119 | "dl1.24xlarge": 13.109040, 120 | // f1 family 121 | "f1.16xlarge": 13.200000, "f1.2xlarge": 1.650000, "f1.4xlarge": 3.300000, 122 | // f2 family 123 | "f2.12xlarge": 3.960000, "f2.48xlarge": 15.840000, "f2.6xlarge": 1.980000, 124 | // g2 family 125 | "g2.2xlarge": 0.650000, "g2.8xlarge": 2.600000, 126 | // g3 family 127 | "g3.16xlarge": 4.560000, "g3.4xlarge": 1.140000, "g3.8xlarge": 2.280000, 128 | // g3s family 129 | "g3s.xlarge": 0.750000, 130 | // g4ad family 131 | "g4ad.16xlarge": 3.468000, "g4ad.2xlarge": 0.541170, "g4ad.4xlarge": 0.867000, "g4ad.8xlarge": 1.734000, 132 | "g4ad.xlarge": 0.378530, 133 | // g4dn family 134 | "g4dn.12xlarge": 3.912000, "g4dn.16xlarge": 4.352000, "g4dn.2xlarge": 0.752000, "g4dn.4xlarge": 1.204000, 135 | "g4dn.8xlarge": 2.176000, "g4dn.metal": 7.824000, "g4dn.xlarge": 0.526000, 136 | // g5 family 137 | "g5.12xlarge": 5.672000, "g5.16xlarge": 4.096000, "g5.24xlarge": 8.144000, "g5.2xlarge": 1.212000, 138 | "g5.48xlarge": 16.288000, "g5.4xlarge": 1.624000, "g5.8xlarge": 2.448000, "g5.xlarge": 1.006000, 139 | // g5g family 140 | "g5g.16xlarge": 2.744000, "g5g.2xlarge": 0.556000, "g5g.4xlarge": 0.828000, "g5g.8xlarge": 1.372000, 141 | "g5g.metal": 2.744000, "g5g.xlarge": 0.420000, 142 | // g6 family 143 | "g6.12xlarge": 4.601600, "g6.16xlarge": 3.396800, "g6.24xlarge": 6.675200, "g6.2xlarge": 0.977600, 144 | "g6.48xlarge": 13.350400, "g6.4xlarge": 1.323200, "g6.8xlarge": 2.014400, "g6.xlarge": 0.804800, 145 | // g6e family 146 | "g6e.12xlarge": 10.492640, "g6e.16xlarge": 7.577190, "g6e.24xlarge": 15.065590, "g6e.2xlarge": 2.242080, 147 | "g6e.48xlarge": 30.131180, "g6e.4xlarge": 3.004240, "g6e.8xlarge": 4.528560, "g6e.xlarge": 1.861000, 148 | // gr6 family 149 | "gr6.4xlarge": 1.539200, "gr6.8xlarge": 2.446400, 150 | // h1 family 151 | "h1.16xlarge": 3.744000, "h1.2xlarge": 0.468000, "h1.4xlarge": 0.936000, "h1.8xlarge": 1.872000, 152 | // hpc7g family 153 | "hpc7g.16xlarge": 1.683200, "hpc7g.4xlarge": 1.683200, "hpc7g.8xlarge": 1.683200, 154 | // i2 family 155 | "i2.2xlarge": 1.705000, "i2.4xlarge": 3.410000, "i2.8xlarge": 6.820000, "i2.xlarge": 0.853000, 156 | // i3 family 157 | "i3.16xlarge": 4.992000, "i3.2xlarge": 0.624000, "i3.4xlarge": 1.248000, "i3.8xlarge": 2.496000, 158 | "i3.large": 0.156000, "i3.metal": 4.992000, "i3.xlarge": 0.312000, 159 | // i3en family 160 | "i3en.12xlarge": 5.424000, "i3en.24xlarge": 10.848000, "i3en.2xlarge": 0.904000, "i3en.3xlarge": 1.356000, 161 | "i3en.6xlarge": 2.712000, "i3en.large": 0.226000, "i3en.metal": 10.848000, "i3en.xlarge": 0.452000, 162 | // i4g family 163 | "i4g.16xlarge": 4.942080, "i4g.2xlarge": 0.617760, "i4g.4xlarge": 1.235520, "i4g.8xlarge": 2.471040, 164 | "i4g.large": 0.154440, "i4g.xlarge": 0.308880, 165 | // i4i family 166 | "i4i.12xlarge": 4.118000, "i4i.16xlarge": 5.491000, "i4i.24xlarge": 8.236800, "i4i.2xlarge": 0.686000, 167 | "i4i.32xlarge": 10.982400, "i4i.4xlarge": 1.373000, "i4i.8xlarge": 2.746000, "i4i.large": 0.172000, 168 | "i4i.metal": 10.982000, "i4i.xlarge": 0.343000, 169 | // i7ie family 170 | "i7ie.12xlarge": 6.237600, "i7ie.18xlarge": 9.356400, "i7ie.24xlarge": 12.475200, "i7ie.2xlarge": 1.039600, 171 | "i7ie.3xlarge": 1.559400, "i7ie.48xlarge": 24.950400, "i7ie.6xlarge": 3.118800, "i7ie.large": 0.259900, 172 | "i7ie.metal-24xl": 13.284400, "i7ie.metal-48xl": 26.568700, "i7ie.xlarge": 0.519800, 173 | // i8g family 174 | "i8g.12xlarge": 4.118400, "i8g.16xlarge": 5.491200, "i8g.24xlarge": 8.236800, "i8g.2xlarge": 0.686400, 175 | "i8g.48xlarge": 16.473600, "i8g.4xlarge": 1.372800, "i8g.8xlarge": 2.745600, "i8g.large": 0.171600, 176 | "i8g.metal-24xl": 9.060480, "i8g.xlarge": 0.343200, 177 | // im4gn family 178 | "im4gn.16xlarge": 5.820670, "im4gn.2xlarge": 0.727580, "im4gn.4xlarge": 1.455170, "im4gn.8xlarge": 2.910340, 179 | "im4gn.large": 0.181900, "im4gn.xlarge": 0.363790, 180 | // inf1 family 181 | "inf1.24xlarge": 4.721000, "inf1.2xlarge": 0.362000, "inf1.6xlarge": 1.180000, "inf1.xlarge": 0.228000, 182 | // inf2 family 183 | "inf2.24xlarge": 6.490630, "inf2.48xlarge": 12.981270, "inf2.8xlarge": 1.967860, "inf2.xlarge": 0.758200, 184 | // is4gen family 185 | "is4gen.2xlarge": 1.152600, "is4gen.4xlarge": 2.305200, "is4gen.8xlarge": 4.610400, 186 | "is4gen.large": 0.288150, "is4gen.medium": 0.144080, "is4gen.xlarge": 0.576300, 187 | // m1 family 188 | "m1.large": 0.175000, "m1.medium": 0.087000, "m1.small": 0.044000, "m1.xlarge": 0.350000, 189 | // m2 family 190 | "m2.2xlarge": 0.490000, "m2.4xlarge": 0.980000, "m2.xlarge": 0.245000, 191 | // m3 family 192 | "m3.2xlarge": 0.532000, "m3.large": 0.133000, "m3.medium": 0.067000, "m3.xlarge": 0.266000, 193 | // m4 family 194 | "m4.10xlarge": 2.000000, "m4.16xlarge": 3.200000, "m4.2xlarge": 0.400000, "m4.4xlarge": 0.800000, 195 | "m4.large": 0.100000, "m4.xlarge": 0.200000, 196 | // m5 family 197 | "m5.12xlarge": 2.304000, "m5.16xlarge": 3.072000, "m5.24xlarge": 4.608000, "m5.2xlarge": 0.384000, 198 | "m5.4xlarge": 0.768000, "m5.8xlarge": 1.536000, "m5.large": 0.096000, "m5.metal": 4.608000, 199 | "m5.xlarge": 0.192000, 200 | // m5a family 201 | "m5a.12xlarge": 2.064000, "m5a.16xlarge": 2.752000, "m5a.24xlarge": 4.128000, "m5a.2xlarge": 0.344000, 202 | "m5a.4xlarge": 0.688000, "m5a.8xlarge": 1.376000, "m5a.large": 0.086000, "m5a.xlarge": 0.172000, 203 | // m5ad family 204 | "m5ad.12xlarge": 2.472000, "m5ad.16xlarge": 3.296000, "m5ad.24xlarge": 4.944000, "m5ad.2xlarge": 0.412000, 205 | "m5ad.4xlarge": 0.824000, "m5ad.8xlarge": 1.648000, "m5ad.large": 0.103000, "m5ad.xlarge": 0.206000, 206 | // m5d family 207 | "m5d.12xlarge": 2.712000, "m5d.16xlarge": 3.616000, "m5d.24xlarge": 5.424000, "m5d.2xlarge": 0.452000, 208 | "m5d.4xlarge": 0.904000, "m5d.8xlarge": 1.808000, "m5d.large": 0.113000, "m5d.metal": 5.424000, 209 | "m5d.xlarge": 0.226000, 210 | // m5dn family 211 | "m5dn.12xlarge": 3.264000, "m5dn.16xlarge": 4.352000, "m5dn.24xlarge": 6.528000, "m5dn.2xlarge": 0.544000, 212 | "m5dn.4xlarge": 1.088000, "m5dn.8xlarge": 2.176000, "m5dn.large": 0.136000, "m5dn.metal": 6.528000, 213 | "m5dn.xlarge": 0.272000, 214 | // m5n family 215 | "m5n.12xlarge": 2.856000, "m5n.16xlarge": 3.808000, "m5n.24xlarge": 5.712000, "m5n.2xlarge": 0.476000, 216 | "m5n.4xlarge": 0.952000, "m5n.8xlarge": 1.904000, "m5n.large": 0.119000, "m5n.metal": 5.712000, 217 | "m5n.xlarge": 0.238000, 218 | // m5zn family 219 | "m5zn.12xlarge": 3.964100, "m5zn.2xlarge": 0.660700, "m5zn.3xlarge": 0.991000, "m5zn.6xlarge": 1.982000, 220 | "m5zn.large": 0.165200, "m5zn.metal": 4.360500, "m5zn.xlarge": 0.330300, 221 | // m6a family 222 | "m6a.12xlarge": 2.073600, "m6a.16xlarge": 2.764800, "m6a.24xlarge": 4.147200, "m6a.2xlarge": 0.345600, 223 | "m6a.32xlarge": 5.529600, "m6a.48xlarge": 8.294400, "m6a.4xlarge": 0.691200, "m6a.8xlarge": 1.382400, 224 | "m6a.large": 0.086400, "m6a.metal": 8.294400, "m6a.xlarge": 0.172800, 225 | // m6g family 226 | "m6g.12xlarge": 1.848000, "m6g.16xlarge": 2.464000, "m6g.2xlarge": 0.308000, "m6g.4xlarge": 0.616000, 227 | "m6g.8xlarge": 1.232000, "m6g.large": 0.077000, "m6g.medium": 0.038500, "m6g.metal": 2.611200, 228 | "m6g.xlarge": 0.154000, 229 | // m6gd family 230 | "m6gd.12xlarge": 2.169600, "m6gd.16xlarge": 2.892800, "m6gd.2xlarge": 0.361600, "m6gd.4xlarge": 0.723200, 231 | "m6gd.8xlarge": 1.446400, "m6gd.large": 0.090400, "m6gd.medium": 0.045200, "m6gd.metal": 3.066400, 232 | "m6gd.xlarge": 0.180800, 233 | // m6i family 234 | "m6i.12xlarge": 2.304000, "m6i.16xlarge": 3.072000, "m6i.24xlarge": 4.608000, "m6i.2xlarge": 0.384000, 235 | "m6i.32xlarge": 6.144000, "m6i.4xlarge": 0.768000, "m6i.8xlarge": 1.536000, "m6i.large": 0.096000, 236 | "m6i.metal": 6.144000, "m6i.xlarge": 0.192000, 237 | // m6id family 238 | "m6id.12xlarge": 2.847600, "m6id.16xlarge": 3.796800, "m6id.24xlarge": 5.695200, "m6id.2xlarge": 0.474600, 239 | "m6id.32xlarge": 7.593600, "m6id.4xlarge": 0.949200, "m6id.8xlarge": 1.898400, "m6id.large": 0.118650, 240 | "m6id.metal": 7.593600, "m6id.xlarge": 0.237300, 241 | // m6idn family 242 | "m6idn.12xlarge": 3.818880, "m6idn.16xlarge": 5.091840, "m6idn.24xlarge": 7.637760, 243 | "m6idn.2xlarge": 0.636480, "m6idn.32xlarge": 10.183680, "m6idn.4xlarge": 1.272960, "m6idn.8xlarge": 2.545920, 244 | "m6idn.large": 0.159120, "m6idn.metal": 10.183680, "m6idn.xlarge": 0.318240, 245 | // m6in family 246 | "m6in.12xlarge": 3.341520, "m6in.16xlarge": 4.455360, "m6in.24xlarge": 6.683040, "m6in.2xlarge": 0.556920, 247 | "m6in.32xlarge": 8.910720, "m6in.4xlarge": 1.113840, "m6in.8xlarge": 2.227680, "m6in.large": 0.139230, 248 | "m6in.metal": 8.910720, "m6in.xlarge": 0.278460, 249 | // m7a family 250 | "m7a.12xlarge": 2.782080, "m7a.16xlarge": 3.709440, "m7a.24xlarge": 5.564160, "m7a.2xlarge": 0.463680, 251 | "m7a.32xlarge": 7.418880, "m7a.48xlarge": 11.128320, "m7a.4xlarge": 0.927360, "m7a.8xlarge": 1.854720, 252 | "m7a.large": 0.115920, "m7a.medium": 0.057960, "m7a.metal-48xl": 11.128320, "m7a.xlarge": 0.231840, 253 | // m7g family 254 | "m7g.12xlarge": 1.958400, "m7g.16xlarge": 2.611200, "m7g.2xlarge": 0.326400, "m7g.4xlarge": 0.652800, 255 | "m7g.8xlarge": 1.305600, "m7g.large": 0.081600, "m7g.medium": 0.040800, "m7g.metal": 2.611200, 256 | "m7g.xlarge": 0.163200, 257 | // m7gd family 258 | "m7gd.12xlarge": 2.562800, "m7gd.16xlarge": 3.417100, "m7gd.2xlarge": 0.427100, "m7gd.4xlarge": 0.854300, 259 | "m7gd.8xlarge": 1.708600, "m7gd.large": 0.106800, "m7gd.medium": 0.053400, "m7gd.metal": 3.417100, 260 | "m7gd.xlarge": 0.213600, 261 | // m7i-flex family 262 | "m7i-flex.12xlarge": 2.298240, "m7i-flex.16xlarge": 3.064320, "m7i-flex.2xlarge": 0.383040, 263 | "m7i-flex.4xlarge": 0.766080, "m7i-flex.8xlarge": 1.532160, "m7i-flex.large": 0.095760, 264 | "m7i-flex.xlarge": 0.191520, 265 | // m7i family 266 | "m7i.12xlarge": 2.419200, "m7i.16xlarge": 3.225600, "m7i.24xlarge": 4.838400, "m7i.2xlarge": 0.403200, 267 | "m7i.48xlarge": 9.676800, "m7i.4xlarge": 0.806400, "m7i.8xlarge": 1.612800, "m7i.large": 0.100800, 268 | "m7i.metal-24xl": 5.322240, "m7i.metal-48xl": 9.676800, "m7i.xlarge": 0.201600, 269 | // m8g family 270 | "m8g.12xlarge": 2.154240, "m8g.16xlarge": 2.872320, "m8g.24xlarge": 4.308480, "m8g.2xlarge": 0.359040, 271 | "m8g.48xlarge": 8.616960, "m8g.4xlarge": 0.718080, "m8g.8xlarge": 1.436160, "m8g.large": 0.089760, 272 | "m8g.medium": 0.044880, "m8g.metal-24xl": 4.739330, "m8g.metal-48xl": 8.616960, "m8g.xlarge": 0.179520, 273 | // p2 family 274 | "p2.16xlarge": 14.400000, "p2.8xlarge": 7.200000, "p2.xlarge": 0.900000, 275 | // p3 family 276 | "p3.16xlarge": 24.480000, "p3.2xlarge": 3.060000, "p3.8xlarge": 12.240000, 277 | // p3dn family 278 | "p3dn.24xlarge": 31.212000, 279 | // p4d family 280 | "p4d.24xlarge": 32.772600, 281 | // p4de family 282 | "p4de.24xlarge": 40.965750, 283 | // p5 family 284 | "p5.48xlarge": 98.320000, 285 | // p5en family 286 | "p5en.48xlarge": 84.800000, 287 | // r3 family 288 | "r3.2xlarge": 0.665000, "r3.4xlarge": 1.330000, "r3.8xlarge": 2.660000, "r3.large": 0.166000, 289 | "r3.xlarge": 0.333000, 290 | // r4 family 291 | "r4.16xlarge": 4.256000, "r4.2xlarge": 0.532000, "r4.4xlarge": 1.064000, "r4.8xlarge": 2.128000, 292 | "r4.large": 0.133000, "r4.xlarge": 0.266000, 293 | // r5 family 294 | "r5.12xlarge": 3.024000, "r5.16xlarge": 4.032000, "r5.24xlarge": 6.048000, "r5.2xlarge": 0.504000, 295 | "r5.4xlarge": 1.008000, "r5.8xlarge": 2.016000, "r5.large": 0.126000, "r5.metal": 6.048000, 296 | "r5.xlarge": 0.252000, 297 | // r5a family 298 | "r5a.12xlarge": 2.712000, "r5a.16xlarge": 3.616000, "r5a.24xlarge": 5.424000, "r5a.2xlarge": 0.452000, 299 | "r5a.4xlarge": 0.904000, "r5a.8xlarge": 1.808000, "r5a.large": 0.113000, "r5a.xlarge": 0.226000, 300 | // r5ad family 301 | "r5ad.12xlarge": 3.144000, "r5ad.16xlarge": 4.192000, "r5ad.24xlarge": 6.288000, "r5ad.2xlarge": 0.524000, 302 | "r5ad.4xlarge": 1.048000, "r5ad.8xlarge": 2.096000, "r5ad.large": 0.131000, "r5ad.xlarge": 0.262000, 303 | // r5b family 304 | "r5b.12xlarge": 3.576000, "r5b.16xlarge": 4.768000, "r5b.24xlarge": 7.152000, "r5b.2xlarge": 0.596000, 305 | "r5b.4xlarge": 1.192000, "r5b.8xlarge": 2.384000, "r5b.large": 0.149000, "r5b.metal": 7.867200, 306 | "r5b.xlarge": 0.298000, 307 | // r5d family 308 | "r5d.12xlarge": 3.456000, "r5d.16xlarge": 4.608000, "r5d.24xlarge": 6.912000, "r5d.2xlarge": 0.576000, 309 | "r5d.4xlarge": 1.152000, "r5d.8xlarge": 2.304000, "r5d.large": 0.144000, "r5d.metal": 6.912000, 310 | "r5d.xlarge": 0.288000, 311 | // r5dn family 312 | "r5dn.12xlarge": 4.008000, "r5dn.16xlarge": 5.344000, "r5dn.24xlarge": 8.016000, "r5dn.2xlarge": 0.668000, 313 | "r5dn.4xlarge": 1.336000, "r5dn.8xlarge": 2.672000, "r5dn.large": 0.167000, "r5dn.metal": 8.016000, 314 | "r5dn.xlarge": 0.334000, 315 | // r5n family 316 | "r5n.12xlarge": 3.576000, "r5n.16xlarge": 4.768000, "r5n.24xlarge": 7.152000, "r5n.2xlarge": 0.596000, 317 | "r5n.4xlarge": 1.192000, "r5n.8xlarge": 2.384000, "r5n.large": 0.149000, "r5n.metal": 7.152000, 318 | "r5n.xlarge": 0.298000, 319 | // r6a family 320 | "r6a.12xlarge": 2.721600, "r6a.16xlarge": 3.628800, "r6a.24xlarge": 5.443200, "r6a.2xlarge": 0.453600, 321 | "r6a.32xlarge": 7.257600, "r6a.48xlarge": 10.886400, "r6a.4xlarge": 0.907200, "r6a.8xlarge": 1.814400, 322 | "r6a.large": 0.113400, "r6a.metal": 10.886400, "r6a.xlarge": 0.226800, 323 | // r6g family 324 | "r6g.12xlarge": 2.419200, "r6g.16xlarge": 3.225600, "r6g.2xlarge": 0.403200, "r6g.4xlarge": 0.806400, 325 | "r6g.8xlarge": 1.612800, "r6g.large": 0.100800, "r6g.medium": 0.050400, "r6g.metal": 3.419100, 326 | "r6g.xlarge": 0.201600, 327 | // r6gd family 328 | "r6gd.12xlarge": 2.764800, "r6gd.16xlarge": 3.686400, "r6gd.2xlarge": 0.460800, "r6gd.4xlarge": 0.921600, 329 | "r6gd.8xlarge": 1.843200, "r6gd.large": 0.115200, "r6gd.medium": 0.057600, "r6gd.metal": 3.907600, 330 | "r6gd.xlarge": 0.230400, 331 | // r6i family 332 | "r6i.12xlarge": 3.024000, "r6i.16xlarge": 4.032000, "r6i.24xlarge": 6.048000, "r6i.2xlarge": 0.504000, 333 | "r6i.32xlarge": 8.064000, "r6i.4xlarge": 1.008000, "r6i.8xlarge": 2.016000, "r6i.large": 0.126000, 334 | "r6i.metal": 8.064000, "r6i.xlarge": 0.252000, 335 | // r6id family 336 | "r6id.12xlarge": 3.628800, "r6id.16xlarge": 4.838400, "r6id.24xlarge": 7.257600, "r6id.2xlarge": 0.604800, 337 | "r6id.32xlarge": 9.676800, "r6id.4xlarge": 1.209600, "r6id.8xlarge": 2.419200, "r6id.large": 0.151200, 338 | "r6id.metal": 9.676800, "r6id.xlarge": 0.302400, 339 | // r6idn family 340 | "r6idn.12xlarge": 4.689360, "r6idn.16xlarge": 6.252480, "r6idn.24xlarge": 9.378720, 341 | "r6idn.2xlarge": 0.781560, "r6idn.32xlarge": 12.504960, "r6idn.4xlarge": 1.563120, "r6idn.8xlarge": 3.126240, 342 | "r6idn.large": 0.195390, "r6idn.metal": 12.504960, "r6idn.xlarge": 0.390780, 343 | // r6in family 344 | "r6in.12xlarge": 4.183920, "r6in.16xlarge": 5.578560, "r6in.24xlarge": 8.367840, "r6in.2xlarge": 0.697320, 345 | "r6in.32xlarge": 11.157120, "r6in.4xlarge": 1.394640, "r6in.8xlarge": 2.789280, "r6in.large": 0.174330, 346 | "r6in.metal": 11.157120, "r6in.xlarge": 0.348660, 347 | // r7a family 348 | "r7a.12xlarge": 3.651600, "r7a.16xlarge": 4.868800, "r7a.24xlarge": 7.303200, "r7a.2xlarge": 0.608600, 349 | "r7a.32xlarge": 9.737600, "r7a.48xlarge": 14.606400, "r7a.4xlarge": 1.217200, "r7a.8xlarge": 2.434400, 350 | "r7a.large": 0.152150, "r7a.medium": 0.076080, "r7a.metal-48xl": 14.606400, "r7a.xlarge": 0.304300, 351 | // r7g family 352 | "r7g.12xlarge": 2.570400, "r7g.16xlarge": 3.427200, "r7g.2xlarge": 0.428400, "r7g.4xlarge": 0.856800, 353 | "r7g.8xlarge": 1.713600, "r7g.large": 0.107100, "r7g.medium": 0.053600, "r7g.metal": 3.427200, 354 | "r7g.xlarge": 0.214200, 355 | // r7gd family 356 | "r7gd.12xlarge": 3.265900, "r7gd.16xlarge": 4.354600, "r7gd.2xlarge": 0.544300, "r7gd.4xlarge": 1.088600, 357 | "r7gd.8xlarge": 2.177300, "r7gd.large": 0.136100, "r7gd.medium": 0.068000, "r7gd.metal": 4.354600, 358 | "r7gd.xlarge": 0.272200, 359 | // r7i family 360 | "r7i.12xlarge": 3.175200, "r7i.16xlarge": 4.233600, "r7i.24xlarge": 6.350400, "r7i.2xlarge": 0.529200, 361 | "r7i.48xlarge": 12.700800, "r7i.4xlarge": 1.058400, "r7i.8xlarge": 2.116800, "r7i.large": 0.132300, 362 | "r7i.metal-24xl": 6.985440, "r7i.metal-48xl": 12.700800, "r7i.xlarge": 0.264600, 363 | // r7iz family 364 | "r7iz.12xlarge": 4.464000, "r7iz.16xlarge": 5.952000, "r7iz.2xlarge": 0.744000, "r7iz.32xlarge": 11.904000, 365 | "r7iz.4xlarge": 1.488000, "r7iz.8xlarge": 2.976000, "r7iz.large": 0.186000, "r7iz.metal-16xl": 6.547200, 366 | "r7iz.metal-32xl": 13.094400, "r7iz.xlarge": 0.372000, 367 | // r8g family 368 | "r8g.12xlarge": 2.827680, "r8g.16xlarge": 3.770240, "r8g.24xlarge": 5.655360, "r8g.2xlarge": 0.471280, 369 | "r8g.48xlarge": 11.310720, "r8g.4xlarge": 0.942560, "r8g.8xlarge": 1.885120, "r8g.large": 0.117820, 370 | "r8g.medium": 0.058910, "r8g.metal-24xl": 6.220900, "r8g.metal-48xl": 11.310720, "r8g.xlarge": 0.235640, 371 | // t1 family 372 | "t1.micro": 0.020000, 373 | // t2 family 374 | "t2.2xlarge": 0.371200, "t2.large": 0.092800, "t2.medium": 0.046400, "t2.micro": 0.011600, 375 | "t2.nano": 0.005800, "t2.small": 0.023000, "t2.xlarge": 0.185600, 376 | // t3 family 377 | "t3.2xlarge": 0.332800, "t3.large": 0.083200, "t3.medium": 0.041600, "t3.micro": 0.010400, 378 | "t3.nano": 0.005200, "t3.small": 0.020800, "t3.xlarge": 0.166400, 379 | // t3a family 380 | "t3a.2xlarge": 0.300800, "t3a.large": 0.075200, "t3a.medium": 0.037600, "t3a.micro": 0.009400, 381 | "t3a.nano": 0.004700, "t3a.small": 0.018800, "t3a.xlarge": 0.150400, 382 | // t4g family 383 | "t4g.2xlarge": 0.268800, "t4g.large": 0.067200, "t4g.medium": 0.033600, "t4g.micro": 0.008400, 384 | "t4g.nano": 0.004200, "t4g.small": 0.016800, "t4g.xlarge": 0.134400, 385 | // trn1 family 386 | "trn1.2xlarge": 1.343750, "trn1.32xlarge": 21.500000, 387 | // trn1n family 388 | "trn1n.32xlarge": 24.780000, 389 | // u-12tb1 family 390 | "u-12tb1.112xlarge": 109.200000, 391 | // u-18tb1 family 392 | "u-18tb1.112xlarge": 163.800000, 393 | // u-24tb1 family 394 | "u-24tb1.112xlarge": 218.400000, 395 | // u-3tb1 family 396 | "u-3tb1.56xlarge": 27.300000, 397 | // u-6tb1 family 398 | "u-6tb1.112xlarge": 54.600000, "u-6tb1.56xlarge": 46.403910, 399 | // u-9tb1 family 400 | "u-9tb1.112xlarge": 81.900000, 401 | // u7i-12tb family 402 | "u7i-12tb.224xlarge": 135.227750, 403 | // u7i-6tb family 404 | "u7i-6tb.112xlarge": 62.790000, 405 | // u7i-8tb family 406 | "u7i-8tb.112xlarge": 83.720000, 407 | // u7in-16tb family 408 | "u7in-16tb.224xlarge": 180.475580, 409 | // u7in-24tb family 410 | "u7in-24tb.224xlarge": 270.731280, 411 | // u7in-32tb family 412 | "u7in-32tb.224xlarge": 360.986950, 413 | // vt1 family 414 | "vt1.24xlarge": 5.200000, "vt1.3xlarge": 0.650000, "vt1.6xlarge": 1.300000, 415 | // x1 family 416 | "x1.16xlarge": 6.669000, "x1.32xlarge": 13.338000, 417 | // x1e family 418 | "x1e.16xlarge": 13.344000, "x1e.2xlarge": 1.668000, "x1e.32xlarge": 26.688000, "x1e.4xlarge": 3.336000, 419 | "x1e.8xlarge": 6.672000, "x1e.xlarge": 0.834000, 420 | // x2gd family 421 | "x2gd.12xlarge": 4.008000, "x2gd.16xlarge": 5.344000, "x2gd.2xlarge": 0.668000, "x2gd.4xlarge": 1.336000, 422 | "x2gd.8xlarge": 2.672000, "x2gd.large": 0.167000, "x2gd.medium": 0.083500, "x2gd.metal": 5.878400, 423 | "x2gd.xlarge": 0.334000, 424 | // x2idn family 425 | "x2idn.16xlarge": 6.669000, "x2idn.24xlarge": 10.003500, "x2idn.32xlarge": 13.338000, 426 | "x2idn.metal": 13.338000, 427 | // x2iedn family 428 | "x2iedn.16xlarge": 13.338000, "x2iedn.24xlarge": 20.007000, "x2iedn.2xlarge": 1.667250, 429 | "x2iedn.32xlarge": 26.676000, "x2iedn.4xlarge": 3.334500, "x2iedn.8xlarge": 6.669000, 430 | "x2iedn.metal": 26.676000, "x2iedn.xlarge": 0.833630, 431 | // x2iezn family 432 | "x2iezn.12xlarge": 10.008000, "x2iezn.2xlarge": 1.668000, "x2iezn.4xlarge": 3.336000, 433 | "x2iezn.6xlarge": 5.004000, "x2iezn.8xlarge": 6.672000, "x2iezn.metal": 10.008000, 434 | // x8g family 435 | "x8g.12xlarge": 4.689600, "x8g.16xlarge": 6.252800, "x8g.24xlarge": 9.379200, "x8g.2xlarge": 0.781600, 436 | "x8g.48xlarge": 18.758400, "x8g.4xlarge": 1.563200, "x8g.8xlarge": 3.126400, "x8g.large": 0.195400, 437 | "x8g.medium": 0.097700, "x8g.metal-24xl": 10.317120, "x8g.metal-48xl": 18.758400, "x8g.xlarge": 0.390800, 438 | // z1d family 439 | "z1d.12xlarge": 4.464000, "z1d.2xlarge": 0.744000, "z1d.3xlarge": 1.116000, "z1d.6xlarge": 2.232000, 440 | "z1d.large": 0.186000, "z1d.metal": 4.464000, "z1d.xlarge": 0.372000, 441 | }, 442 | } 443 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= 2 | github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= 3 | github.com/Pallinder/go-randomdata v1.2.0 h1:DZ41wBchNRb/0GfsePLiSwb0PHZmT67XY00lCDlaYPg= 4 | github.com/Pallinder/go-randomdata v1.2.0/go.mod h1:yHmJgulpD2Nfrm0cR9tI/+oAgRqCQQixsA8HyRZfV9Y= 5 | github.com/avast/retry-go v3.0.0+incompatible h1:4SOWQ7Qs+oroOTQOYnAHqelpCO0biHSxpiH9JdtuBj0= 6 | github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY= 7 | github.com/aws/aws-sdk-go-v2 v1.40.1 h1:difXb4maDZkRH0x//Qkwcfpdg1XQVXEAEs2DdXldFFc= 8 | github.com/aws/aws-sdk-go-v2 v1.40.1/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0= 9 | github.com/aws/aws-sdk-go-v2/config v1.32.3 h1:cpz7H2uMNTDa0h/5CYL5dLUEzPSLo2g0NkbxTRJtSSU= 10 | github.com/aws/aws-sdk-go-v2/config v1.32.3/go.mod h1:srtPKaJJe3McW6T/+GMBZyIPc+SeqJsNPJsd4mOYZ6s= 11 | github.com/aws/aws-sdk-go-v2/credentials v1.19.3 h1:01Ym72hK43hjwDeJUfi1l2oYLXBAOR8gNSZNmXmvuas= 12 | github.com/aws/aws-sdk-go-v2/credentials v1.19.3/go.mod h1:55nWF/Sr9Zvls0bGnWkRxUdhzKqj9uRNlPvgV1vgxKc= 13 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.15 h1:utxLraaifrSBkeyII9mIbVwXXWrZdlPO7FIKmyLCEcY= 14 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.15/go.mod h1:hW6zjYUDQwfz3icf4g2O41PHi77u10oAzJ84iSzR/lo= 15 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.15 h1:Y5YXgygXwDI5P4RkteB5yF7v35neH7LfJKBG+hzIons= 16 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.15/go.mod h1:K+/1EpG42dFSY7CBj+Fruzm8PsCGWTXJ3jdeJ659oGQ= 17 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.15 h1:AvltKnW9ewxX2hFmQS0FyJH93aSvJVUEFvXfU+HWtSE= 18 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.15/go.mod h1:3I4oCdZdmgrREhU74qS1dK9yZ62yumob+58AbFR4cQA= 19 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk= 20 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc= 21 | github.com/aws/aws-sdk-go-v2/service/ec2 v1.275.1 h1:nEpHPUp2UKzxiLBoaLLTnIrWBmb1OL0vf8KHDHjNqcQ= 22 | github.com/aws/aws-sdk-go-v2/service/ec2 v1.275.1/go.mod h1:6xabBAflTTz4OO5f/P4QJrjzZ0WTYjRka+ZWXFqWw8U= 23 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E= 24 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow= 25 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.15 h1:3/u/4yZOffg5jdNk1sDpOQ4Y+R6Xbh+GzpDrSZjuy3U= 26 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.15/go.mod h1:4Zkjq0FKjE78NKjabuM4tRXKFzUJWXgP0ItEZK8l7JU= 27 | github.com/aws/aws-sdk-go-v2/service/pricing v1.40.8 h1:vGtbD2OJBCWyskzNfdNGInhmsfTDTzprkptY7bvMxjY= 28 | github.com/aws/aws-sdk-go-v2/service/pricing v1.40.8/go.mod h1:fPmD3rMZaMgKgUor3jiOr+fzCaNGE+T8vJJVeoXArMA= 29 | github.com/aws/aws-sdk-go-v2/service/signin v1.0.3 h1:d/6xOGIllc/XW1lzG9a4AUBMmpLA9PXcQnVPTuHHcik= 30 | github.com/aws/aws-sdk-go-v2/service/signin v1.0.3/go.mod h1:fQ7E7Qj9GiW8y0ClD7cUJk3Bz5Iw8wZkWDHsTe8vDKs= 31 | github.com/aws/aws-sdk-go-v2/service/sso v1.30.6 h1:8sTTiw+9yuNXcfWeqKF2x01GqCF49CpP4Z9nKrrk/ts= 32 | github.com/aws/aws-sdk-go-v2/service/sso v1.30.6/go.mod h1:8WYg+Y40Sn3X2hioaaWAAIngndR8n1XFdRPPX+7QBaM= 33 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.11 h1:E+KqWoVsSrj1tJ6I/fjDIu5xoS2Zacuu1zT+H7KtiIk= 34 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.11/go.mod h1:qyWHz+4lvkXcr3+PoGlGHEI+3DLLiU6/GdrFfMaAhB0= 35 | github.com/aws/aws-sdk-go-v2/service/sts v1.41.3 h1:tzMkjh0yTChUqJDgGkcDdxvZDSrJ/WB6R6ymI5ehqJI= 36 | github.com/aws/aws-sdk-go-v2/service/sts v1.41.3/go.mod h1:T270C0R5sZNLbWUe8ueiAF42XSZxxPocTaGSgs5c/60= 37 | github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk= 38 | github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= 39 | github.com/awslabs/operatorpkg v0.0.0-20250909182303-e8e550b6f339 h1:p4oSlQ9IaT7/DHfgcrs9zdNhdIp37VIMujZLuxSgECk= 40 | github.com/awslabs/operatorpkg v0.0.0-20250909182303-e8e550b6f339/go.mod h1:tNmCf0qIjaGbODGbm3DM8GIKBUvvxM7iW3KHbpSnVgw= 41 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= 42 | github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= 43 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 44 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 45 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 46 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 47 | github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs= 48 | github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg= 49 | github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= 50 | github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= 51 | github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= 52 | github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= 53 | github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ= 54 | github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao= 55 | github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= 56 | github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= 57 | github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ= 58 | github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE= 59 | github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8= 60 | github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= 61 | github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= 62 | github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= 63 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 64 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 65 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 66 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 67 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 68 | github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= 69 | github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= 70 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= 71 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= 72 | github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U= 73 | github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= 74 | github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= 75 | github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= 76 | github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb h1:IT4JYU7k4ikYg1SCxNI1/Tieq/NFvh6dzLdgi7eu0tM= 77 | github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb/go.mod h1:bH6Xx7IW64qjjJq8M2u4dxNaBiDfKK+z/3eGDpXEQhc= 78 | github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= 79 | github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= 80 | github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= 81 | github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= 82 | github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= 83 | github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 84 | github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= 85 | github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= 86 | github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= 87 | github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= 88 | github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= 89 | github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= 90 | github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= 91 | github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= 92 | github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= 93 | github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= 94 | github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= 95 | github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= 96 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 97 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 98 | github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= 99 | github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= 100 | github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= 101 | github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= 102 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 103 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 104 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 105 | github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= 106 | github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 107 | github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6 h1:EEHtgt9IwisQ2AZ4pIsMjahcegHh6rmhqxzIRQIyepY= 108 | github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6/go.mod h1:I6V7YzU0XDpsHqbsyrghnFZLO1gwK6NPTNvmetQIk9U= 109 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 110 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 111 | github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= 112 | github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= 113 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 114 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 115 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 116 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 117 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 118 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 119 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 120 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 121 | github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= 122 | github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= 123 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 124 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 125 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 126 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 127 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 128 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 129 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 130 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 131 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 132 | github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= 133 | github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 134 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 135 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 136 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 137 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 138 | github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75 h1:P8UmIzZMYDR+NGImiFvErt6VWfIRPuGM+vyjiEdkmIw= 139 | github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= 140 | github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= 141 | github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 142 | github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= 143 | github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= 144 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 145 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 146 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 147 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 148 | github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= 149 | github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 150 | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= 151 | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= 152 | github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= 153 | github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= 154 | github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= 155 | github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= 156 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 157 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 158 | github.com/onsi/ginkgo/v2 v2.25.3 h1:Ty8+Yi/ayDAGtk4XxmmfUy4GabvM+MegeB4cDLRi6nw= 159 | github.com/onsi/ginkgo/v2 v2.25.3/go.mod h1:43uiyQC4Ed2tkOzLsEYm7hnrb7UJTWHYNsuy3bG/snE= 160 | github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= 161 | github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= 162 | github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= 163 | github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= 164 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 165 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 166 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 167 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 168 | github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= 169 | github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= 170 | github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= 171 | github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= 172 | github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= 173 | github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= 174 | github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= 175 | github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= 176 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 177 | github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= 178 | github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 179 | github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= 180 | github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= 181 | github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= 182 | github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= 183 | github.com/samber/lo v1.51.0 h1:kysRYLbHy/MB7kQZf5DSN50JHmMsNEdeY24VzJFu7wI= 184 | github.com/samber/lo v1.51.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= 185 | github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= 186 | github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= 187 | github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= 188 | github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 189 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 190 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 191 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 192 | github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= 193 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 194 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 195 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 196 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 197 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 198 | github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= 199 | github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 200 | github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= 201 | github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= 202 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= 203 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= 204 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 205 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 206 | go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= 207 | go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= 208 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 209 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 210 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 211 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 212 | go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= 213 | go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= 214 | go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= 215 | go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= 216 | go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= 217 | go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= 218 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 219 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 220 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 221 | golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= 222 | golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= 223 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 224 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 225 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 226 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 227 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 228 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 229 | golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= 230 | golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= 231 | golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= 232 | golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= 233 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 234 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 235 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 236 | golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= 237 | golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 238 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 239 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 240 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 241 | golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 242 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 243 | golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= 244 | golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 245 | golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= 246 | golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= 247 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 248 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 249 | golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= 250 | golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= 251 | golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI= 252 | golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= 253 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 254 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 255 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 256 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 257 | golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= 258 | golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= 259 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 260 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 261 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 262 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 263 | gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= 264 | gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= 265 | google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= 266 | google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= 267 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 268 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 269 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 270 | gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= 271 | gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= 272 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 273 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 274 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 275 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 276 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 277 | k8s.io/api v0.34.2 h1:fsSUNZhV+bnL6Aqrp6O7lMTy6o5x2C4XLjnh//8SLYY= 278 | k8s.io/api v0.34.2/go.mod h1:MMBPaWlED2a8w4RSeanD76f7opUoypY8TFYkSM+3XHw= 279 | k8s.io/apiextensions-apiserver v0.34.1 h1:NNPBva8FNAPt1iSVwIE0FsdrVriRXMsaWFMqJbII2CI= 280 | k8s.io/apiextensions-apiserver v0.34.1/go.mod h1:hP9Rld3zF5Ay2Of3BeEpLAToP+l4s5UlxiHfqRaRcMc= 281 | k8s.io/apimachinery v0.34.2 h1:zQ12Uk3eMHPxrsbUJgNF8bTauTVR2WgqJsTmwTE/NW4= 282 | k8s.io/apimachinery v0.34.2/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= 283 | k8s.io/client-go v0.34.2 h1:Co6XiknN+uUZqiddlfAjT68184/37PS4QAzYvQvDR8M= 284 | k8s.io/client-go v0.34.2/go.mod h1:2VYDl1XXJsdcAxw7BenFslRQX28Dxz91U9MWKjX97fE= 285 | k8s.io/component-base v0.34.1 h1:v7xFgG+ONhytZNFpIz5/kecwD+sUhVE6HU7qQUiRM4A= 286 | k8s.io/component-base v0.34.1/go.mod h1:mknCpLlTSKHzAQJJnnHVKqjxR7gBeHRv0rPXA7gdtQ0= 287 | k8s.io/component-helpers v0.34.1 h1:gWhH3CCdwAx5P3oJqZKb4Lg5FYZTWVbdWtOI8n9U4XY= 288 | k8s.io/component-helpers v0.34.1/go.mod h1:4VgnUH7UA/shuBur+OWoQC0xfb69sy/93ss0ybZqm3c= 289 | k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= 290 | k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= 291 | k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA= 292 | k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts= 293 | k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y= 294 | k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= 295 | sigs.k8s.io/controller-runtime v0.22.1 h1:Ah1T7I+0A7ize291nJZdS1CabF/lB4E++WizgV24Eqg= 296 | sigs.k8s.io/controller-runtime v0.22.1/go.mod h1:FwiwRjkRPbiN+zp2QRp7wlTCzbUXxZ/D4OzuQUDwBHY= 297 | sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= 298 | sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= 299 | sigs.k8s.io/karpenter v1.8.0 h1:AmTHUPtnuL8IX9mbcD3NOohyk62idrBCBtM+8Wn6Jvk= 300 | sigs.k8s.io/karpenter v1.8.0/go.mod h1:nDDVB5873dVVuyTam3oJrllSv0sAgp6as6/5HRTcV4o= 301 | sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= 302 | sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= 303 | sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= 304 | sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= 305 | sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= 306 | sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= 307 | -------------------------------------------------------------------------------- /pkg/aws/zz_generated_aws_us_gov.pricing.go: -------------------------------------------------------------------------------- 1 | //go:build !ignore_autogenerated 2 | 3 | /* 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package aws 18 | 19 | // generated at 2025-04-07T13:14:43Z for us-east-1 20 | 21 | import ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types" 22 | 23 | var InitialOnDemandPricesUSGov = map[string]map[ec2types.InstanceType]float64{ 24 | // us-gov-east-1 25 | "us-gov-east-1": { 26 | // c5 family 27 | "c5.12xlarge": 2.448000, "c5.18xlarge": 3.672000, "c5.24xlarge": 4.896000, "c5.2xlarge": 0.408000, 28 | "c5.4xlarge": 0.816000, "c5.9xlarge": 1.836000, "c5.large": 0.102000, "c5.metal": 4.896000, 29 | "c5.xlarge": 0.204000, 30 | // c5a family 31 | "c5a.12xlarge": 2.208000, "c5a.16xlarge": 2.944000, "c5a.24xlarge": 4.416000, "c5a.2xlarge": 0.368000, 32 | "c5a.4xlarge": 0.736000, "c5a.8xlarge": 1.472000, "c5a.large": 0.092000, "c5a.xlarge": 0.184000, 33 | // c5d family 34 | "c5d.18xlarge": 4.176000, "c5d.2xlarge": 0.464000, "c5d.4xlarge": 0.928000, "c5d.9xlarge": 2.088000, 35 | "c5d.large": 0.116000, "c5d.xlarge": 0.232000, 36 | // c5n family 37 | "c5n.18xlarge": 4.680000, "c5n.2xlarge": 0.520000, "c5n.4xlarge": 1.040000, "c5n.9xlarge": 2.340000, 38 | "c5n.large": 0.130000, "c5n.metal": 4.680000, "c5n.xlarge": 0.260000, 39 | // c6g family 40 | "c6g.12xlarge": 1.958400, "c6g.16xlarge": 2.611200, "c6g.2xlarge": 0.326400, "c6g.4xlarge": 0.652800, 41 | "c6g.8xlarge": 1.305600, "c6g.large": 0.081600, "c6g.medium": 0.040800, "c6g.metal": 2.767900, 42 | "c6g.xlarge": 0.163200, 43 | // c6gd family 44 | "c6gd.12xlarge": 2.227200, "c6gd.16xlarge": 2.969600, "c6gd.2xlarge": 0.371200, "c6gd.4xlarge": 0.742400, 45 | "c6gd.8xlarge": 1.484800, "c6gd.large": 0.092800, "c6gd.medium": 0.046400, "c6gd.metal": 2.969600, 46 | "c6gd.xlarge": 0.185600, 47 | // c6gn family 48 | "c6gn.12xlarge": 2.496000, "c6gn.16xlarge": 3.328000, "c6gn.2xlarge": 0.416000, "c6gn.4xlarge": 0.832000, 49 | "c6gn.8xlarge": 1.664000, "c6gn.large": 0.104000, "c6gn.medium": 0.052000, "c6gn.xlarge": 0.208000, 50 | // c6i family 51 | "c6i.12xlarge": 2.448000, "c6i.16xlarge": 3.264000, "c6i.24xlarge": 4.896000, "c6i.2xlarge": 0.408000, 52 | "c6i.32xlarge": 6.528000, "c6i.4xlarge": 0.816000, "c6i.8xlarge": 1.632000, "c6i.large": 0.102000, 53 | "c6i.metal": 6.528000, "c6i.xlarge": 0.204000, 54 | // c6in family 55 | "c6in.12xlarge": 3.276000, "c6in.16xlarge": 4.368000, "c6in.24xlarge": 6.552000, "c6in.2xlarge": 0.546000, 56 | "c6in.32xlarge": 8.736000, "c6in.4xlarge": 1.092000, "c6in.8xlarge": 2.184000, "c6in.large": 0.136500, 57 | "c6in.metal": 8.736000, "c6in.xlarge": 0.273000, 58 | // c7g family 59 | "c7g.12xlarge": 2.080800, "c7g.16xlarge": 2.774400, "c7g.2xlarge": 0.346800, "c7g.4xlarge": 0.693600, 60 | "c7g.8xlarge": 1.387200, "c7g.large": 0.086700, "c7g.medium": 0.043400, "c7g.metal": 2.774400, 61 | "c7g.xlarge": 0.173400, 62 | // c7gd family 63 | "c7gd.12xlarge": 2.630900, "c7gd.16xlarge": 3.507800, "c7gd.2xlarge": 0.438500, "c7gd.4xlarge": 0.877000, 64 | "c7gd.8xlarge": 1.753900, "c7gd.large": 0.109600, "c7gd.medium": 0.054800, "c7gd.metal": 3.507800, 65 | "c7gd.xlarge": 0.219200, 66 | // c7i family 67 | "c7i.12xlarge": 2.570400, "c7i.16xlarge": 3.427200, "c7i.24xlarge": 5.140800, "c7i.2xlarge": 0.428400, 68 | "c7i.48xlarge": 10.281600, "c7i.4xlarge": 0.856800, "c7i.8xlarge": 1.713600, "c7i.large": 0.107100, 69 | "c7i.metal-24xl": 5.654880, "c7i.metal-48xl": 10.281600, "c7i.xlarge": 0.214200, 70 | // d2 family 71 | "d2.2xlarge": 1.656000, "d2.4xlarge": 3.312000, "d2.8xlarge": 6.624000, "d2.xlarge": 0.828000, 72 | // g4dn family 73 | "g4dn.12xlarge": 4.931000, "g4dn.16xlarge": 5.486000, "g4dn.2xlarge": 0.948000, "g4dn.4xlarge": 1.518000, 74 | "g4dn.8xlarge": 2.743000, "g4dn.xlarge": 0.663000, 75 | // hpc6a family 76 | "hpc6a.48xlarge": 3.467000, 77 | // i3 family 78 | "i3.16xlarge": 6.016000, "i3.2xlarge": 0.752000, "i3.4xlarge": 1.504000, "i3.8xlarge": 3.008000, 79 | "i3.large": 0.188000, "i3.metal": 6.016000, "i3.xlarge": 0.376000, 80 | // i3en family 81 | "i3en.12xlarge": 6.552000, "i3en.24xlarge": 13.104000, "i3en.2xlarge": 1.092000, "i3en.3xlarge": 1.638000, 82 | "i3en.6xlarge": 3.276000, "i3en.large": 0.273000, "i3en.metal": 13.104000, "i3en.xlarge": 0.546000, 83 | // i4i family 84 | "i4i.12xlarge": 4.963000, "i4i.16xlarge": 6.618000, "i4i.24xlarge": 9.926400, "i4i.2xlarge": 0.827000, 85 | "i4i.32xlarge": 13.235200, "i4i.4xlarge": 1.654000, "i4i.8xlarge": 3.309000, "i4i.large": 0.207000, 86 | "i4i.metal": 13.235000, "i4i.xlarge": 0.414000, 87 | // inf1 family 88 | "inf1.24xlarge": 5.953000, "inf1.2xlarge": 0.456000, "inf1.6xlarge": 1.488000, "inf1.xlarge": 0.288000, 89 | // m5 family 90 | "m5.12xlarge": 2.904000, "m5.16xlarge": 3.872000, "m5.24xlarge": 5.808000, "m5.2xlarge": 0.484000, 91 | "m5.4xlarge": 0.968000, "m5.8xlarge": 1.936000, "m5.large": 0.121000, "m5.metal": 5.808000, 92 | "m5.xlarge": 0.242000, 93 | // m5a family 94 | "m5a.12xlarge": 2.616000, "m5a.16xlarge": 3.488000, "m5a.24xlarge": 5.232000, "m5a.2xlarge": 0.436000, 95 | "m5a.4xlarge": 0.872000, "m5a.8xlarge": 1.744000, "m5a.large": 0.109000, "m5a.xlarge": 0.218000, 96 | // m5d family 97 | "m5d.12xlarge": 3.432000, "m5d.16xlarge": 4.576000, "m5d.24xlarge": 6.864000, "m5d.2xlarge": 0.572000, 98 | "m5d.4xlarge": 1.144000, "m5d.8xlarge": 2.288000, "m5d.large": 0.143000, "m5d.metal": 6.864000, 99 | "m5d.xlarge": 0.286000, 100 | // m5dn family 101 | "m5dn.12xlarge": 4.104000, "m5dn.16xlarge": 5.472000, "m5dn.24xlarge": 8.208000, "m5dn.2xlarge": 0.684000, 102 | "m5dn.4xlarge": 1.368000, "m5dn.8xlarge": 2.736000, "m5dn.large": 0.171000, "m5dn.metal": 8.208000, 103 | "m5dn.xlarge": 0.342000, 104 | // m5n family 105 | "m5n.12xlarge": 3.576000, "m5n.16xlarge": 4.768000, "m5n.24xlarge": 7.152000, "m5n.2xlarge": 0.596000, 106 | "m5n.4xlarge": 1.192000, "m5n.8xlarge": 2.384000, "m5n.large": 0.149000, "m5n.metal": 7.152000, 107 | "m5n.xlarge": 0.298000, 108 | // m6g family 109 | "m6g.12xlarge": 2.323200, "m6g.16xlarge": 3.097600, "m6g.2xlarge": 0.387200, "m6g.4xlarge": 0.774400, 110 | "m6g.8xlarge": 1.548800, "m6g.large": 0.096800, "m6g.medium": 0.048400, "m6g.metal": 3.283500, 111 | "m6g.xlarge": 0.193600, 112 | // m6gd family 113 | "m6gd.12xlarge": 2.745600, "m6gd.16xlarge": 3.660800, "m6gd.2xlarge": 0.457600, "m6gd.4xlarge": 0.915200, 114 | "m6gd.8xlarge": 1.830400, "m6gd.large": 0.114400, "m6gd.medium": 0.057200, "m6gd.metal": 3.880400, 115 | "m6gd.xlarge": 0.228800, 116 | // m6i family 117 | "m6i.12xlarge": 2.904000, "m6i.16xlarge": 3.872000, "m6i.24xlarge": 5.808000, "m6i.2xlarge": 0.484000, 118 | "m6i.32xlarge": 7.744000, "m6i.4xlarge": 0.968000, "m6i.8xlarge": 1.936000, "m6i.large": 0.121000, 119 | "m6i.metal": 7.744000, "m6i.xlarge": 0.242000, 120 | // m7g family 121 | "m7g.12xlarge": 2.467200, "m7g.16xlarge": 3.289600, "m7g.2xlarge": 0.411200, "m7g.4xlarge": 0.822400, 122 | "m7g.8xlarge": 1.644800, "m7g.large": 0.102800, "m7g.medium": 0.051400, "m7g.metal": 3.289600, 123 | "m7g.xlarge": 0.205600, 124 | // m7i-flex family 125 | "m7i-flex.12xlarge": 2.896800, "m7i-flex.16xlarge": 3.862400, "m7i-flex.2xlarge": 0.482800, 126 | "m7i-flex.4xlarge": 0.965600, "m7i-flex.8xlarge": 1.931200, "m7i-flex.large": 0.120700, 127 | "m7i-flex.xlarge": 0.241400, 128 | // m7i family 129 | "m7i.12xlarge": 3.049200, "m7i.16xlarge": 4.065600, "m7i.24xlarge": 6.098400, "m7i.2xlarge": 0.508200, 130 | "m7i.48xlarge": 12.196800, "m7i.4xlarge": 1.016400, "m7i.8xlarge": 2.032800, "m7i.large": 0.127050, 131 | "m7i.metal-24xl": 6.708240, "m7i.metal-48xl": 12.196800, "m7i.xlarge": 0.254100, 132 | // p3dn family 133 | "p3dn.24xlarge": 37.454000, 134 | // r5 family 135 | "r5.12xlarge": 3.624000, "r5.16xlarge": 4.832000, "r5.24xlarge": 7.248000, "r5.2xlarge": 0.604000, 136 | "r5.4xlarge": 1.208000, "r5.8xlarge": 2.416000, "r5.large": 0.151000, "r5.metal": 7.248000, 137 | "r5.xlarge": 0.302000, 138 | // r5a family 139 | "r5a.12xlarge": 3.264000, "r5a.16xlarge": 4.352000, "r5a.24xlarge": 6.528000, "r5a.2xlarge": 0.544000, 140 | "r5a.4xlarge": 1.088000, "r5a.8xlarge": 2.176000, "r5a.large": 0.136000, "r5a.xlarge": 0.272000, 141 | // r5d family 142 | "r5d.12xlarge": 4.152000, "r5d.16xlarge": 5.536000, "r5d.24xlarge": 8.304000, "r5d.2xlarge": 0.692000, 143 | "r5d.4xlarge": 1.384000, "r5d.8xlarge": 2.768000, "r5d.large": 0.173000, "r5d.metal": 8.304000, 144 | "r5d.xlarge": 0.346000, 145 | // r5dn family 146 | "r5dn.12xlarge": 4.824000, "r5dn.16xlarge": 6.432000, "r5dn.24xlarge": 9.648000, "r5dn.2xlarge": 0.804000, 147 | "r5dn.4xlarge": 1.608000, "r5dn.8xlarge": 3.216000, "r5dn.large": 0.201000, "r5dn.metal": 9.648000, 148 | "r5dn.xlarge": 0.402000, 149 | // r5n family 150 | "r5n.12xlarge": 4.296000, "r5n.16xlarge": 5.728000, "r5n.24xlarge": 8.592000, "r5n.2xlarge": 0.716000, 151 | "r5n.4xlarge": 1.432000, "r5n.8xlarge": 2.864000, "r5n.large": 0.179000, "r5n.metal": 8.592000, 152 | "r5n.xlarge": 0.358000, 153 | // r6g family 154 | "r6g.12xlarge": 2.899200, "r6g.16xlarge": 3.865600, "r6g.2xlarge": 0.483200, "r6g.4xlarge": 0.966400, 155 | "r6g.8xlarge": 1.932800, "r6g.large": 0.120800, "r6g.medium": 0.060400, "r6g.metal": 4.097500, 156 | "r6g.xlarge": 0.241600, 157 | // r6gd family 158 | "r6gd.12xlarge": 3.321600, "r6gd.16xlarge": 4.428800, "r6gd.2xlarge": 0.553600, "r6gd.4xlarge": 1.107200, 159 | "r6gd.8xlarge": 2.214400, "r6gd.large": 0.138400, "r6gd.medium": 0.069200, "r6gd.metal": 4.428800, 160 | "r6gd.xlarge": 0.276800, 161 | // r6i family 162 | "r6i.12xlarge": 3.624000, "r6i.16xlarge": 4.832000, "r6i.24xlarge": 7.248000, "r6i.2xlarge": 0.604000, 163 | "r6i.32xlarge": 9.664000, "r6i.4xlarge": 1.208000, "r6i.8xlarge": 2.416000, "r6i.large": 0.151000, 164 | "r6i.metal": 9.664000, "r6i.xlarge": 0.302000, 165 | // r7gd family 166 | "r7gd.12xlarge": 3.923500, "r7gd.16xlarge": 5.231400, "r7gd.2xlarge": 0.653900, "r7gd.4xlarge": 1.307800, 167 | "r7gd.8xlarge": 2.615700, "r7gd.large": 0.163500, "r7gd.medium": 0.081700, "r7gd.metal": 5.231400, 168 | "r7gd.xlarge": 0.327000, 169 | // r7i family 170 | "r7i.12xlarge": 3.805200, "r7i.16xlarge": 5.073600, "r7i.24xlarge": 7.610400, "r7i.2xlarge": 0.634200, 171 | "r7i.48xlarge": 15.220800, "r7i.4xlarge": 1.268400, "r7i.8xlarge": 2.536800, "r7i.large": 0.158550, 172 | "r7i.metal-24xl": 8.371440, "r7i.metal-48xl": 15.220800, "r7i.xlarge": 0.317100, 173 | // t3 family 174 | "t3.2xlarge": 0.390400, "t3.large": 0.097600, "t3.medium": 0.048800, "t3.micro": 0.012200, 175 | "t3.nano": 0.006100, "t3.small": 0.024400, "t3.xlarge": 0.195200, 176 | // t3a family 177 | "t3a.2xlarge": 0.351400, "t3a.large": 0.087800, "t3a.medium": 0.043900, "t3a.micro": 0.011000, 178 | "t3a.nano": 0.005500, "t3a.small": 0.022000, "t3a.xlarge": 0.175700, 179 | // t4g family 180 | "t4g.2xlarge": 0.313600, "t4g.large": 0.078400, "t4g.medium": 0.039200, "t4g.micro": 0.009800, 181 | "t4g.nano": 0.004900, "t4g.small": 0.019600, "t4g.xlarge": 0.156800, 182 | // u-12tb1 family 183 | "u-12tb1.112xlarge": 130.867000, 184 | // u-24tb1 family 185 | "u-24tb1.112xlarge": 261.730000, 186 | // u-6tb1 family 187 | "u-6tb1.112xlarge": 65.433000, "u-6tb1.56xlarge": 55.610750, 188 | // u-9tb1 family 189 | "u-9tb1.112xlarge": 98.150000, 190 | // x1 family 191 | "x1.16xlarge": 8.003000, "x1.32xlarge": 16.006000, 192 | // x1e family 193 | "x1e.16xlarge": 16.000000, "x1e.2xlarge": 2.000000, "x1e.32xlarge": 32.000000, "x1e.4xlarge": 4.000000, 194 | "x1e.8xlarge": 8.000000, "x1e.xlarge": 1.000000, 195 | // x2idn family 196 | "x2idn.16xlarge": 8.003000, "x2idn.24xlarge": 12.004500, "x2idn.32xlarge": 16.006000, 197 | "x2idn.metal": 16.006000, 198 | // x2iedn family 199 | "x2iedn.16xlarge": 16.006000, "x2iedn.24xlarge": 24.009000, "x2iedn.2xlarge": 2.000750, 200 | "x2iedn.32xlarge": 32.012000, "x2iedn.4xlarge": 4.001500, "x2iedn.8xlarge": 8.003000, 201 | "x2iedn.metal": 32.012000, "x2iedn.xlarge": 1.000380, 202 | }, 203 | 204 | // us-gov-west-1 205 | "us-gov-west-1": { 206 | // c1 family 207 | "c1.medium": 0.157000, "c1.xlarge": 0.628000, 208 | // c3 family 209 | "c3.2xlarge": 0.504000, "c3.4xlarge": 1.008000, "c3.8xlarge": 2.016000, "c3.large": 0.126000, 210 | "c3.xlarge": 0.252000, 211 | // c4 family 212 | "c4.2xlarge": 0.479000, "c4.4xlarge": 0.958000, "c4.8xlarge": 1.915000, "c4.large": 0.120000, 213 | "c4.xlarge": 0.239000, 214 | // c5 family 215 | "c5.12xlarge": 2.448000, "c5.18xlarge": 3.672000, "c5.24xlarge": 4.896000, "c5.2xlarge": 0.408000, 216 | "c5.4xlarge": 0.816000, "c5.9xlarge": 1.836000, "c5.large": 0.102000, "c5.metal": 4.896000, 217 | "c5.xlarge": 0.204000, 218 | // c5a family 219 | "c5a.12xlarge": 2.208000, "c5a.16xlarge": 2.944000, "c5a.24xlarge": 4.416000, "c5a.2xlarge": 0.368000, 220 | "c5a.4xlarge": 0.736000, "c5a.8xlarge": 1.472000, "c5a.large": 0.092000, "c5a.xlarge": 0.184000, 221 | // c5d family 222 | "c5d.12xlarge": 2.784000, "c5d.18xlarge": 4.176000, "c5d.24xlarge": 5.568000, "c5d.2xlarge": 0.464000, 223 | "c5d.4xlarge": 0.928000, "c5d.9xlarge": 2.088000, "c5d.large": 0.116000, "c5d.metal": 5.568000, 224 | "c5d.xlarge": 0.232000, 225 | // c5n family 226 | "c5n.18xlarge": 4.680000, "c5n.2xlarge": 0.520000, "c5n.4xlarge": 1.040000, "c5n.9xlarge": 2.340000, 227 | "c5n.large": 0.130000, "c5n.metal": 4.680000, "c5n.xlarge": 0.260000, 228 | // c6g family 229 | "c6g.12xlarge": 1.958400, "c6g.16xlarge": 2.611200, "c6g.2xlarge": 0.326400, "c6g.4xlarge": 0.652800, 230 | "c6g.8xlarge": 1.305600, "c6g.large": 0.081600, "c6g.medium": 0.040800, "c6g.metal": 2.767900, 231 | "c6g.xlarge": 0.163200, 232 | // c6gd family 233 | "c6gd.12xlarge": 2.227200, "c6gd.16xlarge": 2.969600, "c6gd.2xlarge": 0.371200, "c6gd.4xlarge": 0.742400, 234 | "c6gd.8xlarge": 1.484800, "c6gd.large": 0.092800, "c6gd.medium": 0.046400, "c6gd.metal": 2.969600, 235 | "c6gd.xlarge": 0.185600, 236 | // c6gn family 237 | "c6gn.12xlarge": 2.496000, "c6gn.16xlarge": 3.328000, "c6gn.2xlarge": 0.416000, "c6gn.4xlarge": 0.832000, 238 | "c6gn.8xlarge": 1.664000, "c6gn.large": 0.104000, "c6gn.medium": 0.052000, "c6gn.xlarge": 0.208000, 239 | // c6i family 240 | "c6i.12xlarge": 2.448000, "c6i.16xlarge": 3.264000, "c6i.24xlarge": 4.896000, "c6i.2xlarge": 0.408000, 241 | "c6i.32xlarge": 6.528000, "c6i.4xlarge": 0.816000, "c6i.8xlarge": 1.632000, "c6i.large": 0.102000, 242 | "c6i.metal": 6.528000, "c6i.xlarge": 0.204000, 243 | // c6id family 244 | "c6id.12xlarge": 2.923200, "c6id.16xlarge": 3.897600, "c6id.24xlarge": 5.846400, "c6id.2xlarge": 0.487200, 245 | "c6id.32xlarge": 7.795200, "c6id.4xlarge": 0.974400, "c6id.8xlarge": 1.948800, "c6id.large": 0.121800, 246 | "c6id.metal": 7.795200, "c6id.xlarge": 0.243600, 247 | // c6in family 248 | "c6in.12xlarge": 3.276000, "c6in.16xlarge": 4.368000, "c6in.24xlarge": 6.552000, "c6in.2xlarge": 0.546000, 249 | "c6in.32xlarge": 8.736000, "c6in.4xlarge": 1.092000, "c6in.8xlarge": 2.184000, "c6in.large": 0.136500, 250 | "c6in.metal": 8.736000, "c6in.xlarge": 0.273000, 251 | // c7g family 252 | "c7g.12xlarge": 2.080800, "c7g.16xlarge": 2.774400, "c7g.2xlarge": 0.346800, "c7g.4xlarge": 0.693600, 253 | "c7g.8xlarge": 1.387200, "c7g.large": 0.086700, "c7g.medium": 0.043400, "c7g.metal": 2.774400, 254 | "c7g.xlarge": 0.173400, 255 | // c7i-flex family 256 | "c7i-flex.12xlarge": 2.442000, "c7i-flex.16xlarge": 3.256000, "c7i-flex.2xlarge": 0.407000, 257 | "c7i-flex.4xlarge": 0.814000, "c7i-flex.8xlarge": 1.628000, "c7i-flex.large": 0.101750, 258 | "c7i-flex.xlarge": 0.203500, 259 | // c7i family 260 | "c7i.12xlarge": 2.570400, "c7i.16xlarge": 3.427200, "c7i.24xlarge": 5.140800, "c7i.2xlarge": 0.428400, 261 | "c7i.48xlarge": 10.281600, "c7i.4xlarge": 0.856800, "c7i.8xlarge": 1.713600, "c7i.large": 0.107100, 262 | "c7i.metal-24xl": 5.654880, "c7i.metal-48xl": 10.281600, "c7i.xlarge": 0.214200, 263 | // cc2 family 264 | "cc2.8xlarge": 2.250000, 265 | // d2 family 266 | "d2.2xlarge": 1.656000, "d2.4xlarge": 3.312000, "d2.8xlarge": 6.624000, "d2.xlarge": 0.828000, 267 | // d3 family 268 | "d3.2xlarge": 1.197000, "d3.4xlarge": 2.394000, "d3.8xlarge": 4.787760, "d3.xlarge": 0.598000, 269 | // f1 family 270 | "f1.16xlarge": 15.840000, "f1.2xlarge": 1.980000, "f1.4xlarge": 3.960000, 271 | // g3 family 272 | "g3.16xlarge": 5.280000, "g3.4xlarge": 1.320000, "g3.8xlarge": 2.640000, 273 | // g3s family 274 | "g3s.xlarge": 0.868000, 275 | // g4dn family 276 | "g4dn.12xlarge": 4.931000, "g4dn.16xlarge": 5.486000, "g4dn.2xlarge": 0.948000, "g4dn.4xlarge": 1.518000, 277 | "g4dn.8xlarge": 2.743000, "g4dn.metal": 9.862000, "g4dn.xlarge": 0.663000, 278 | // g6 family 279 | "g6.12xlarge": 5.800030, "g6.16xlarge": 4.281450, "g6.24xlarge": 8.413670, "g6.2xlarge": 1.232200, 280 | "g6.48xlarge": 16.827340, "g6.4xlarge": 1.667810, "g6.8xlarge": 2.539030, "g6.xlarge": 1.014400, 281 | // gr6 family 282 | "gr6.4xlarge": 1.940100, "gr6.8xlarge": 3.083590, 283 | // hpc6a family 284 | "hpc6a.48xlarge": 3.467000, 285 | // hpc6id family 286 | "hpc6id.32xlarge": 6.854400, 287 | // hpc7a family 288 | "hpc7a.12xlarge": 8.667400, "hpc7a.24xlarge": 8.667400, "hpc7a.48xlarge": 8.667400, 289 | "hpc7a.96xlarge": 8.667400, 290 | // hpc7g family 291 | "hpc7g.16xlarge": 2.026200, "hpc7g.4xlarge": 2.026200, "hpc7g.8xlarge": 2.026200, 292 | // hs1 family 293 | "hs1.8xlarge": 5.520000, 294 | // i2 family 295 | "i2.2xlarge": 2.046000, "i2.4xlarge": 4.092000, "i2.8xlarge": 8.184000, "i2.xlarge": 1.023000, 296 | // i3 family 297 | "i3.16xlarge": 6.016000, "i3.2xlarge": 0.752000, "i3.4xlarge": 1.504000, "i3.8xlarge": 3.008000, 298 | "i3.large": 0.188000, "i3.metal": 6.016000, "i3.xlarge": 0.376000, 299 | // i3en family 300 | "i3en.12xlarge": 6.552000, "i3en.24xlarge": 13.104000, "i3en.2xlarge": 1.092000, "i3en.3xlarge": 1.638000, 301 | "i3en.6xlarge": 3.276000, "i3en.large": 0.273000, "i3en.metal": 13.104000, "i3en.xlarge": 0.546000, 302 | // i3p family 303 | "i3p.16xlarge": 6.016000, 304 | // i4i family 305 | "i4i.12xlarge": 4.963000, "i4i.16xlarge": 6.618000, "i4i.24xlarge": 9.926400, "i4i.2xlarge": 0.827000, 306 | "i4i.32xlarge": 13.235200, "i4i.4xlarge": 1.654000, "i4i.8xlarge": 3.309000, "i4i.large": 0.207000, 307 | "i4i.metal": 13.235000, "i4i.xlarge": 0.414000, 308 | // inf1 family 309 | "inf1.24xlarge": 5.953000, "inf1.2xlarge": 0.456000, "inf1.6xlarge": 1.488000, "inf1.xlarge": 0.288000, 310 | // m1 family 311 | "m1.large": 0.211000, "m1.medium": 0.106000, "m1.small": 0.053000, "m1.xlarge": 0.423000, 312 | // m2 family 313 | "m2.2xlarge": 0.586000, "m2.4xlarge": 1.171000, "m2.xlarge": 0.293000, 314 | // m3 family 315 | "m3.2xlarge": 0.672000, "m3.large": 0.168000, "m3.medium": 0.084000, "m3.xlarge": 0.336000, 316 | // m4 family 317 | "m4.10xlarge": 2.520000, "m4.16xlarge": 4.032000, "m4.2xlarge": 0.504000, "m4.4xlarge": 1.008000, 318 | "m4.large": 0.126000, "m4.xlarge": 0.252000, 319 | // m5 family 320 | "m5.12xlarge": 2.904000, "m5.16xlarge": 3.872000, "m5.24xlarge": 5.808000, "m5.2xlarge": 0.484000, 321 | "m5.4xlarge": 0.968000, "m5.8xlarge": 1.936000, "m5.large": 0.121000, "m5.metal": 5.808000, 322 | "m5.xlarge": 0.242000, 323 | // m5a family 324 | "m5a.12xlarge": 2.616000, "m5a.16xlarge": 3.488000, "m5a.24xlarge": 5.232000, "m5a.2xlarge": 0.436000, 325 | "m5a.4xlarge": 0.872000, "m5a.8xlarge": 1.744000, "m5a.large": 0.109000, "m5a.xlarge": 0.218000, 326 | // m5ad family 327 | "m5ad.12xlarge": 3.144000, "m5ad.16xlarge": 4.192000, "m5ad.24xlarge": 6.288000, "m5ad.2xlarge": 0.524000, 328 | "m5ad.4xlarge": 1.048000, "m5ad.8xlarge": 2.096000, "m5ad.large": 0.131000, "m5ad.xlarge": 0.262000, 329 | // m5d family 330 | "m5d.12xlarge": 3.432000, "m5d.16xlarge": 4.576000, "m5d.24xlarge": 6.864000, "m5d.2xlarge": 0.572000, 331 | "m5d.4xlarge": 1.144000, "m5d.8xlarge": 2.288000, "m5d.large": 0.143000, "m5d.metal": 6.864000, 332 | "m5d.xlarge": 0.286000, 333 | // m5dn family 334 | "m5dn.12xlarge": 4.104000, "m5dn.16xlarge": 5.472000, "m5dn.24xlarge": 8.208000, "m5dn.2xlarge": 0.684000, 335 | "m5dn.4xlarge": 1.368000, "m5dn.8xlarge": 2.736000, "m5dn.large": 0.171000, "m5dn.metal": 8.208000, 336 | "m5dn.xlarge": 0.342000, 337 | // m5n family 338 | "m5n.12xlarge": 3.576000, "m5n.16xlarge": 4.768000, "m5n.24xlarge": 7.152000, "m5n.2xlarge": 0.596000, 339 | "m5n.4xlarge": 1.192000, "m5n.8xlarge": 2.384000, "m5n.large": 0.149000, "m5n.metal": 7.152000, 340 | "m5n.xlarge": 0.298000, 341 | // m6g family 342 | "m6g.12xlarge": 2.323200, "m6g.16xlarge": 3.097600, "m6g.2xlarge": 0.387200, "m6g.4xlarge": 0.774400, 343 | "m6g.8xlarge": 1.548800, "m6g.large": 0.096800, "m6g.medium": 0.048400, "m6g.metal": 3.283500, 344 | "m6g.xlarge": 0.193600, 345 | // m6gd family 346 | "m6gd.12xlarge": 2.745600, "m6gd.16xlarge": 3.660800, "m6gd.2xlarge": 0.457600, "m6gd.4xlarge": 0.915200, 347 | "m6gd.8xlarge": 1.830400, "m6gd.large": 0.114400, "m6gd.medium": 0.057200, "m6gd.metal": 3.880400, 348 | "m6gd.xlarge": 0.228800, 349 | // m6i family 350 | "m6i.12xlarge": 2.904000, "m6i.16xlarge": 3.872000, "m6i.24xlarge": 5.808000, "m6i.2xlarge": 0.484000, 351 | "m6i.32xlarge": 7.744000, "m6i.4xlarge": 0.968000, "m6i.8xlarge": 1.936000, "m6i.large": 0.121000, 352 | "m6i.metal": 7.744000, "m6i.xlarge": 0.242000, 353 | // m6id family 354 | "m6id.12xlarge": 3.604800, "m6id.16xlarge": 4.806400, "m6id.24xlarge": 7.209600, "m6id.2xlarge": 0.600800, 355 | "m6id.32xlarge": 9.612800, "m6id.4xlarge": 1.201600, "m6id.8xlarge": 2.403200, "m6id.large": 0.150200, 356 | "m6id.metal": 9.612800, "m6id.xlarge": 0.300400, 357 | // m6idn family 358 | "m6idn.12xlarge": 4.801680, "m6idn.16xlarge": 6.402240, "m6idn.24xlarge": 9.603360, 359 | "m6idn.2xlarge": 0.800280, "m6idn.32xlarge": 12.804480, "m6idn.4xlarge": 1.600560, "m6idn.8xlarge": 3.201120, 360 | "m6idn.large": 0.200070, "m6idn.metal": 12.804480, "m6idn.xlarge": 0.400140, 361 | // m6in family 362 | "m6in.12xlarge": 4.183920, "m6in.16xlarge": 5.578560, "m6in.24xlarge": 8.367840, "m6in.2xlarge": 0.697320, 363 | "m6in.32xlarge": 11.157120, "m6in.4xlarge": 1.394640, "m6in.8xlarge": 2.789280, "m6in.large": 0.174330, 364 | "m6in.metal": 11.157120, "m6in.xlarge": 0.348660, 365 | // m7i-flex family 366 | "m7i-flex.12xlarge": 2.896800, "m7i-flex.16xlarge": 3.862400, "m7i-flex.2xlarge": 0.482800, 367 | "m7i-flex.4xlarge": 0.965600, "m7i-flex.8xlarge": 1.931200, "m7i-flex.large": 0.120700, 368 | "m7i-flex.xlarge": 0.241400, 369 | // m7i family 370 | "m7i.12xlarge": 3.049200, "m7i.16xlarge": 4.065600, "m7i.24xlarge": 6.098400, "m7i.2xlarge": 0.508200, 371 | "m7i.48xlarge": 12.196800, "m7i.4xlarge": 1.016400, "m7i.8xlarge": 2.032800, "m7i.large": 0.127050, 372 | "m7i.metal-24xl": 6.708240, "m7i.metal-48xl": 12.196800, "m7i.xlarge": 0.254100, 373 | // p2 family 374 | "p2.16xlarge": 17.280000, "p2.8xlarge": 8.640000, "p2.xlarge": 1.080000, 375 | // p3 family 376 | "p3.16xlarge": 29.376000, "p3.2xlarge": 3.672000, "p3.8xlarge": 14.688000, 377 | // p3dn family 378 | "p3dn.24xlarge": 37.454000, 379 | // p4d family 380 | "p4d.24xlarge": 39.330000, 381 | // p5 family 382 | "p5.48xlarge": 117.984000, 383 | // r3 family 384 | "r3.2xlarge": 0.798000, "r3.4xlarge": 1.596000, "r3.8xlarge": 3.192000, "r3.large": 0.200000, 385 | "r3.xlarge": 0.399000, 386 | // r4 family 387 | "r4.16xlarge": 5.107200, "r4.2xlarge": 0.638400, "r4.4xlarge": 1.276800, "r4.8xlarge": 2.553600, 388 | "r4.large": 0.159600, "r4.xlarge": 0.319200, 389 | // r5 family 390 | "r5.12xlarge": 3.624000, "r5.16xlarge": 4.832000, "r5.24xlarge": 7.248000, "r5.2xlarge": 0.604000, 391 | "r5.4xlarge": 1.208000, "r5.8xlarge": 2.416000, "r5.large": 0.151000, "r5.metal": 7.248000, 392 | "r5.xlarge": 0.302000, 393 | // r5a family 394 | "r5a.12xlarge": 3.264000, "r5a.16xlarge": 4.352000, "r5a.24xlarge": 6.528000, "r5a.2xlarge": 0.544000, 395 | "r5a.4xlarge": 1.088000, "r5a.8xlarge": 2.176000, "r5a.large": 0.136000, "r5a.xlarge": 0.272000, 396 | // r5ad family 397 | "r5ad.12xlarge": 3.792000, "r5ad.16xlarge": 5.056000, "r5ad.24xlarge": 7.584000, "r5ad.2xlarge": 0.632000, 398 | "r5ad.4xlarge": 1.264000, "r5ad.8xlarge": 2.528000, "r5ad.large": 0.158000, "r5ad.xlarge": 0.316000, 399 | // r5d family 400 | "r5d.12xlarge": 4.152000, "r5d.16xlarge": 5.536000, "r5d.24xlarge": 8.304000, "r5d.2xlarge": 0.692000, 401 | "r5d.4xlarge": 1.384000, "r5d.8xlarge": 2.768000, "r5d.large": 0.173000, "r5d.metal": 8.304000, 402 | "r5d.xlarge": 0.346000, 403 | // r5dn family 404 | "r5dn.12xlarge": 4.824000, "r5dn.16xlarge": 6.432000, "r5dn.24xlarge": 9.648000, "r5dn.2xlarge": 0.804000, 405 | "r5dn.4xlarge": 1.608000, "r5dn.8xlarge": 3.216000, "r5dn.large": 0.201000, "r5dn.metal": 9.648000, 406 | "r5dn.xlarge": 0.402000, 407 | // r5n family 408 | "r5n.12xlarge": 4.296000, "r5n.16xlarge": 5.728000, "r5n.24xlarge": 8.592000, "r5n.2xlarge": 0.716000, 409 | "r5n.4xlarge": 1.432000, "r5n.8xlarge": 2.864000, "r5n.large": 0.179000, "r5n.metal": 8.592000, 410 | "r5n.xlarge": 0.358000, 411 | // r6g family 412 | "r6g.12xlarge": 2.899200, "r6g.16xlarge": 3.865600, "r6g.2xlarge": 0.483200, "r6g.4xlarge": 0.966400, 413 | "r6g.8xlarge": 1.932800, "r6g.large": 0.120800, "r6g.medium": 0.060400, "r6g.metal": 4.097500, 414 | "r6g.xlarge": 0.241600, 415 | // r6gd family 416 | "r6gd.12xlarge": 3.321600, "r6gd.16xlarge": 4.428800, "r6gd.2xlarge": 0.553600, "r6gd.4xlarge": 1.107200, 417 | "r6gd.8xlarge": 2.214400, "r6gd.large": 0.138400, "r6gd.medium": 0.069200, "r6gd.metal": 4.428800, 418 | "r6gd.xlarge": 0.276800, 419 | // r6i family 420 | "r6i.12xlarge": 3.624000, "r6i.16xlarge": 4.832000, "r6i.24xlarge": 7.248000, "r6i.2xlarge": 0.604000, 421 | "r6i.32xlarge": 9.664000, "r6i.4xlarge": 1.208000, "r6i.8xlarge": 2.416000, "r6i.large": 0.151000, 422 | "r6i.metal": 9.664000, "r6i.xlarge": 0.302000, 423 | // r6id family 424 | "r6id.12xlarge": 4.360800, "r6id.16xlarge": 5.814400, "r6id.24xlarge": 8.721600, "r6id.2xlarge": 0.726800, 425 | "r6id.32xlarge": 11.628800, "r6id.4xlarge": 1.453600, "r6id.8xlarge": 2.907200, "r6id.large": 0.181700, 426 | "r6id.metal": 11.628800, "r6id.xlarge": 0.363400, 427 | // r6idn family 428 | "r6idn.12xlarge": 5.644080, "r6idn.16xlarge": 7.525440, "r6idn.24xlarge": 11.288160, 429 | "r6idn.2xlarge": 0.940680, "r6idn.32xlarge": 15.050880, "r6idn.4xlarge": 1.881360, "r6idn.8xlarge": 3.762720, 430 | "r6idn.large": 0.235170, "r6idn.metal": 15.050880, "r6idn.xlarge": 0.470340, 431 | // r6in family 432 | "r6in.12xlarge": 5.026320, "r6in.16xlarge": 6.701760, "r6in.24xlarge": 10.052640, "r6in.2xlarge": 0.837720, 433 | "r6in.32xlarge": 13.403520, "r6in.4xlarge": 1.675440, "r6in.8xlarge": 3.350880, "r6in.large": 0.209430, 434 | "r6in.metal": 13.403520, "r6in.xlarge": 0.418860, 435 | // r7g family 436 | "r7g.12xlarge": 3.080600, "r7g.16xlarge": 4.107500, "r7g.2xlarge": 0.513400, "r7g.4xlarge": 1.026900, 437 | "r7g.8xlarge": 2.053800, "r7g.large": 0.128400, "r7g.medium": 0.064200, "r7g.metal": 4.107500, 438 | "r7g.xlarge": 0.256700, 439 | // r7gd family 440 | "r7gd.12xlarge": 3.925000, "r7gd.16xlarge": 5.233300, "r7gd.2xlarge": 0.654200, "r7gd.4xlarge": 1.308300, 441 | "r7gd.8xlarge": 2.616600, "r7gd.large": 0.163500, "r7gd.medium": 0.081800, "r7gd.metal": 5.233300, 442 | "r7gd.xlarge": 0.327100, 443 | // r7i family 444 | "r7i.12xlarge": 3.805200, "r7i.16xlarge": 5.073600, "r7i.24xlarge": 7.610400, "r7i.2xlarge": 0.634200, 445 | "r7i.48xlarge": 15.220800, "r7i.4xlarge": 1.268400, "r7i.8xlarge": 2.536800, "r7i.large": 0.158550, 446 | "r7i.metal-24xl": 8.371440, "r7i.metal-48xl": 15.220800, "r7i.xlarge": 0.317100, 447 | // r8g family 448 | "r8g.12xlarge": 3.388440, "r8g.16xlarge": 4.517920, "r8g.24xlarge": 6.776880, "r8g.2xlarge": 0.564740, 449 | "r8g.48xlarge": 13.553760, "r8g.4xlarge": 1.129480, "r8g.8xlarge": 2.258960, "r8g.large": 0.141190, 450 | "r8g.medium": 0.070590, "r8g.metal-24xl": 7.454570, "r8g.metal-48xl": 13.553760, "r8g.xlarge": 0.282370, 451 | // t1 family 452 | "t1.micro": 0.024000, 453 | // t2 family 454 | "t2.2xlarge": 0.435200, "t2.large": 0.108800, "t2.medium": 0.054400, "t2.micro": 0.013600, 455 | "t2.nano": 0.006800, "t2.small": 0.027200, "t2.xlarge": 0.217600, 456 | // t3 family 457 | "t3.2xlarge": 0.390400, "t3.large": 0.097600, "t3.medium": 0.048800, "t3.micro": 0.012200, 458 | "t3.nano": 0.006100, "t3.small": 0.024400, "t3.xlarge": 0.195200, 459 | // t3a family 460 | "t3a.2xlarge": 0.351400, "t3a.large": 0.087800, "t3a.medium": 0.043900, "t3a.micro": 0.011000, 461 | "t3a.nano": 0.005500, "t3a.small": 0.022000, "t3a.xlarge": 0.175700, 462 | // t4g family 463 | "t4g.2xlarge": 0.313600, "t4g.large": 0.078400, "t4g.medium": 0.039200, "t4g.micro": 0.009800, 464 | "t4g.nano": 0.004900, "t4g.small": 0.019600, "t4g.xlarge": 0.156800, 465 | // u-12tb1 family 466 | "u-12tb1.112xlarge": 130.867000, 467 | // u-24tb1 family 468 | "u-24tb1.112xlarge": 261.730000, 469 | // u-3tb1 family 470 | "u-3tb1.56xlarge": 32.716500, 471 | // u-6tb1 family 472 | "u-6tb1.112xlarge": 65.433000, "u-6tb1.56xlarge": 55.610750, 473 | // u-9tb1 family 474 | "u-9tb1.112xlarge": 98.150000, 475 | // u7in-24tb family 476 | "u7in-24tb.224xlarge": 324.443680, 477 | // x1 family 478 | "x1.16xlarge": 8.003000, "x1.32xlarge": 16.006000, 479 | // x1e family 480 | "x1e.16xlarge": 16.000000, "x1e.2xlarge": 2.000000, "x1e.32xlarge": 32.000000, "x1e.4xlarge": 4.000000, 481 | "x1e.8xlarge": 8.000000, "x1e.xlarge": 1.000000, 482 | // x2idn family 483 | "x2idn.16xlarge": 8.003000, "x2idn.24xlarge": 12.004500, "x2idn.32xlarge": 16.006000, 484 | "x2idn.metal": 16.006000, 485 | // x2iedn family 486 | "x2iedn.16xlarge": 16.006000, "x2iedn.24xlarge": 24.009000, "x2iedn.2xlarge": 2.000750, 487 | "x2iedn.32xlarge": 32.012000, "x2iedn.4xlarge": 4.001500, "x2iedn.8xlarge": 8.003000, 488 | "x2iedn.metal": 32.012000, "x2iedn.xlarge": 1.000380, 489 | }, 490 | } 491 | --------------------------------------------------------------------------------