├── kube ├── hpa ├── eksctl ├── pvc.yml ├── pv.yml ├── deployment.yml ├── cronjob.yml ├── cronjobfail.yml └── job.yml ├── K8sCat ├── yaml │ ├── favicon.ico │ ├── logo192.png │ ├── logo512.png │ ├── robots.txt │ ├── asset-manifest.json │ ├── manifest.json │ ├── index.html │ └── static │ │ ├── css │ │ ├── main.a6b6c53c.css │ │ └── main.a6b6c53c.css.map │ │ └── js │ │ ├── main.e481028a.js.LICENSE.txt │ │ ├── 787.eb3a1d75.chunk.js │ │ └── 787.eb3a1d75.chunk.js.map ├── Assets.xcassets │ ├── Contents.json │ ├── aws.imageset │ │ ├── aws.png │ │ ├── aws@2x.png │ │ ├── aws@3x.png │ │ └── Contents.json │ ├── aliyun.imageset │ │ ├── aliyun.png │ │ ├── aliyun@2x.png │ │ ├── aliyun@3x.png │ │ └── Contents.json │ ├── config.imageset │ │ ├── config.png │ │ ├── config@2x.png │ │ ├── config@3x.png │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── icon-1024.png │ │ ├── icon-29.png │ │ ├── icon-40.png │ │ ├── icon-76.png │ │ ├── icon-20@2x.png │ │ ├── icon-20@3x.png │ │ ├── icon-29@2x.png │ │ ├── icon-29@3x.png │ │ ├── icon-40@2x.png │ │ ├── icon-40@3x.png │ │ ├── icon-60@2x.png │ │ ├── icon-60@3x.png │ │ ├── icon-76@2x.png │ │ ├── icon-20-ipad.png │ │ ├── icon-29-ipad.png │ │ ├── icon-83.5@2x.png │ │ ├── icon-20@2x-ipad.png │ │ ├── icon-29@2x-ipad.png │ │ └── Contents.json │ └── AccentColor.colorset │ │ └── Contents.json ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── id │ ├── AWSResign.swift │ ├── Identifier.swift │ ├── MyAWSClient.swift │ └── Utils.swift ├── K8sCat.entitlements ├── inner │ ├── CaptionText.swift │ ├── LogView.swift │ ├── ExecWebView.swift │ ├── PVCView.swift │ ├── YamlWebView.swift │ ├── ContainerView.swift │ ├── SecretView.swift │ ├── ConfigMapView.swift │ ├── HpaView.swift │ ├── PVView.swift │ ├── JobView.swift │ ├── ReplicaView.swift │ ├── NodeView.swift │ ├── CronJobView.swift │ ├── ServiceView.swift │ ├── DaemonView.swift │ ├── StatefulView.swift │ ├── PodView.swift │ └── DeploymentView.swift ├── K8sCatApp.swift ├── Model.xcdatamodeld │ └── Model.xcdatamodel │ │ └── contents ├── Model+Scale.swift ├── cluster │ ├── NewClusterView.swift │ ├── ClusterTypeView.swift │ ├── ConfigView.swift │ ├── ClusterView.swift │ └── AWSView.swift ├── Core.xcdatamodeld │ └── Core.xcdatamodel │ │ └── contents ├── ContentView.swift ├── SettingView.swift ├── Persistence.swift ├── GlobalView.swift ├── StorageView.swift ├── tab │ └── TabBarButton.swift ├── Ex.swift ├── Model+Controller.swift └── Model+Yaml.swift ├── K8sCat.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ ├── xcuserdata │ │ └── guyanhua.xcuserdatad │ │ │ ├── IDEFindNavigatorScopes.plist │ │ │ └── xcdebugger │ │ │ └── Expressions.xcexplist │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved ├── xcuserdata │ └── guyanhua.xcuserdatad │ │ └── xcschemes │ │ └── xcschememanagement.plist └── xcshareddata │ └── xcschemes │ └── K8sCat.xcscheme ├── K8sCatUITests ├── K8sCatUITestsLaunchTests.swift └── K8sCatUITests.swift ├── PrivacyPolicy.md ├── README.md └── K8sCatTests └── K8sCatTests.swift /kube/hpa: -------------------------------------------------------------------------------- 1 | kubectl autoscale deployment hpa-demo --cpu-percent=10 --min=1 --max=10 -------------------------------------------------------------------------------- /K8sCat/yaml/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buhe/KubeCat/HEAD/K8sCat/yaml/favicon.ico -------------------------------------------------------------------------------- /K8sCat/yaml/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buhe/KubeCat/HEAD/K8sCat/yaml/logo192.png -------------------------------------------------------------------------------- /K8sCat/yaml/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buhe/KubeCat/HEAD/K8sCat/yaml/logo512.png -------------------------------------------------------------------------------- /K8sCat/yaml/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /K8sCat/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /kube/eksctl: -------------------------------------------------------------------------------- 1 | eksctl create cluster --name demo-cat --region us-west-1 2 | eksctl delete cluster --name demo-cat --region us-west-1 -------------------------------------------------------------------------------- /K8sCat/Assets.xcassets/aws.imageset/aws.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buhe/KubeCat/HEAD/K8sCat/Assets.xcassets/aws.imageset/aws.png -------------------------------------------------------------------------------- /K8sCat/Assets.xcassets/aws.imageset/aws@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buhe/KubeCat/HEAD/K8sCat/Assets.xcassets/aws.imageset/aws@2x.png -------------------------------------------------------------------------------- /K8sCat/Assets.xcassets/aws.imageset/aws@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buhe/KubeCat/HEAD/K8sCat/Assets.xcassets/aws.imageset/aws@3x.png -------------------------------------------------------------------------------- /K8sCat/Assets.xcassets/aliyun.imageset/aliyun.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buhe/KubeCat/HEAD/K8sCat/Assets.xcassets/aliyun.imageset/aliyun.png -------------------------------------------------------------------------------- /K8sCat/Assets.xcassets/config.imageset/config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buhe/KubeCat/HEAD/K8sCat/Assets.xcassets/config.imageset/config.png -------------------------------------------------------------------------------- /K8sCat/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /K8sCat/Assets.xcassets/aliyun.imageset/aliyun@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buhe/KubeCat/HEAD/K8sCat/Assets.xcassets/aliyun.imageset/aliyun@2x.png -------------------------------------------------------------------------------- /K8sCat/Assets.xcassets/aliyun.imageset/aliyun@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buhe/KubeCat/HEAD/K8sCat/Assets.xcassets/aliyun.imageset/aliyun@3x.png -------------------------------------------------------------------------------- /K8sCat/Assets.xcassets/config.imageset/config@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buhe/KubeCat/HEAD/K8sCat/Assets.xcassets/config.imageset/config@2x.png -------------------------------------------------------------------------------- /K8sCat/Assets.xcassets/config.imageset/config@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buhe/KubeCat/HEAD/K8sCat/Assets.xcassets/config.imageset/config@3x.png -------------------------------------------------------------------------------- /K8sCat/Assets.xcassets/AppIcon.appiconset/icon-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buhe/KubeCat/HEAD/K8sCat/Assets.xcassets/AppIcon.appiconset/icon-1024.png -------------------------------------------------------------------------------- /K8sCat/Assets.xcassets/AppIcon.appiconset/icon-29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buhe/KubeCat/HEAD/K8sCat/Assets.xcassets/AppIcon.appiconset/icon-29.png -------------------------------------------------------------------------------- /K8sCat/Assets.xcassets/AppIcon.appiconset/icon-40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buhe/KubeCat/HEAD/K8sCat/Assets.xcassets/AppIcon.appiconset/icon-40.png -------------------------------------------------------------------------------- /K8sCat/Assets.xcassets/AppIcon.appiconset/icon-76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buhe/KubeCat/HEAD/K8sCat/Assets.xcassets/AppIcon.appiconset/icon-76.png -------------------------------------------------------------------------------- /K8sCat/Assets.xcassets/AppIcon.appiconset/icon-20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buhe/KubeCat/HEAD/K8sCat/Assets.xcassets/AppIcon.appiconset/icon-20@2x.png -------------------------------------------------------------------------------- /K8sCat/Assets.xcassets/AppIcon.appiconset/icon-20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buhe/KubeCat/HEAD/K8sCat/Assets.xcassets/AppIcon.appiconset/icon-20@3x.png -------------------------------------------------------------------------------- /K8sCat/Assets.xcassets/AppIcon.appiconset/icon-29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buhe/KubeCat/HEAD/K8sCat/Assets.xcassets/AppIcon.appiconset/icon-29@2x.png -------------------------------------------------------------------------------- /K8sCat/Assets.xcassets/AppIcon.appiconset/icon-29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buhe/KubeCat/HEAD/K8sCat/Assets.xcassets/AppIcon.appiconset/icon-29@3x.png -------------------------------------------------------------------------------- /K8sCat/Assets.xcassets/AppIcon.appiconset/icon-40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buhe/KubeCat/HEAD/K8sCat/Assets.xcassets/AppIcon.appiconset/icon-40@2x.png -------------------------------------------------------------------------------- /K8sCat/Assets.xcassets/AppIcon.appiconset/icon-40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buhe/KubeCat/HEAD/K8sCat/Assets.xcassets/AppIcon.appiconset/icon-40@3x.png -------------------------------------------------------------------------------- /K8sCat/Assets.xcassets/AppIcon.appiconset/icon-60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buhe/KubeCat/HEAD/K8sCat/Assets.xcassets/AppIcon.appiconset/icon-60@2x.png -------------------------------------------------------------------------------- /K8sCat/Assets.xcassets/AppIcon.appiconset/icon-60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buhe/KubeCat/HEAD/K8sCat/Assets.xcassets/AppIcon.appiconset/icon-60@3x.png -------------------------------------------------------------------------------- /K8sCat/Assets.xcassets/AppIcon.appiconset/icon-76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buhe/KubeCat/HEAD/K8sCat/Assets.xcassets/AppIcon.appiconset/icon-76@2x.png -------------------------------------------------------------------------------- /K8sCat/Assets.xcassets/AppIcon.appiconset/icon-20-ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buhe/KubeCat/HEAD/K8sCat/Assets.xcassets/AppIcon.appiconset/icon-20-ipad.png -------------------------------------------------------------------------------- /K8sCat/Assets.xcassets/AppIcon.appiconset/icon-29-ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buhe/KubeCat/HEAD/K8sCat/Assets.xcassets/AppIcon.appiconset/icon-29-ipad.png -------------------------------------------------------------------------------- /K8sCat/Assets.xcassets/AppIcon.appiconset/icon-83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buhe/KubeCat/HEAD/K8sCat/Assets.xcassets/AppIcon.appiconset/icon-83.5@2x.png -------------------------------------------------------------------------------- /K8sCat/Assets.xcassets/AppIcon.appiconset/icon-20@2x-ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buhe/KubeCat/HEAD/K8sCat/Assets.xcassets/AppIcon.appiconset/icon-20@2x-ipad.png -------------------------------------------------------------------------------- /K8sCat/Assets.xcassets/AppIcon.appiconset/icon-29@2x-ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buhe/KubeCat/HEAD/K8sCat/Assets.xcassets/AppIcon.appiconset/icon-29@2x-ipad.png -------------------------------------------------------------------------------- /K8sCat.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /K8sCat/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /kube/pvc.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: PersistentVolumeClaim 3 | metadata: 4 | name: default 5 | spec: 6 | accessModes: 7 | - ReadWriteMany 8 | storageClassName: manual 9 | resources: 10 | requests: 11 | storage: 1Gi 12 | -------------------------------------------------------------------------------- /kube/pv.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: PersistentVolume 3 | metadata: 4 | name: default 5 | spec: 6 | storageClassName: manual 7 | capacity: 8 | storage: 1Gi 9 | accessModes: 10 | - ReadWriteMany 11 | nfs: 12 | server: 10.244.1.4 13 | path: "/" 14 | 15 | -------------------------------------------------------------------------------- /K8sCat/id/AWSResign.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AWSResign.swift 3 | // K8sCat 4 | // 5 | // Created by 顾艳华 on 2023/1/8. 6 | // 7 | 8 | import Foundation 9 | 10 | struct AWSResign: Identifiable { 11 | let id: String 12 | let render: String 13 | let value: String 14 | } 15 | 16 | -------------------------------------------------------------------------------- /K8sCat.xcodeproj/project.xcworkspace/xcuserdata/guyanhua.xcuserdatad/IDEFindNavigatorScopes.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /K8sCat.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /kube/deployment.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: hpa-demo 5 | spec: 6 | selector: 7 | matchLabels: 8 | app: nginx 9 | template: 10 | metadata: 11 | labels: 12 | app: nginx 13 | spec: 14 | containers: 15 | - name: nginx 16 | image: nginx 17 | ports: 18 | - containerPort: 80 -------------------------------------------------------------------------------- /K8sCat/K8sCat.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /kube/cronjob.yml: -------------------------------------------------------------------------------- 1 | apiVersion: batch/v1 2 | kind: CronJob 3 | metadata: 4 | name: simplecronjob 5 | spec: 6 | # Run every 10 minutes. See https://en.wikipedia.org/wiki/Cron 7 | schedule: "*/10 * * * *" 8 | jobTemplate: 9 | spec: 10 | template: 11 | spec: 12 | containers: 13 | - name: simplepyscriptapp 14 | image: bmalevich/simplepyscriptapp 15 | restartPolicy: OnFailure -------------------------------------------------------------------------------- /kube/cronjobfail.yml: -------------------------------------------------------------------------------- 1 | apiVersion: batch/v1 2 | kind: CronJob 3 | metadata: 4 | name: simplecronjobfail 5 | spec: 6 | # Run every 10 minutes. See https://en.wikipedia.org/wiki/Cron 7 | schedule: "*/10 * * * *" 8 | jobTemplate: 9 | spec: 10 | template: 11 | spec: 12 | containers: 13 | - name: simplepyscriptapp 14 | image: bmalevich/simplepyscriptappfail 15 | restartPolicy: OnFailure -------------------------------------------------------------------------------- /K8sCat/inner/CaptionText.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CaptionText.swift 3 | // K8sCat 4 | // 5 | // Created by 顾艳华 on 2022/12/27. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct CaptionText: View { 11 | let text: String 12 | var body: some View { 13 | Text(text).font(.caption) 14 | } 15 | } 16 | 17 | struct CaptionText_Previews: PreviewProvider { 18 | static var previews: some View { 19 | CaptionText(text: "123") 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /K8sCat/Assets.xcassets/aws.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "aws.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "aws@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "aws@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /kube/job.yml: -------------------------------------------------------------------------------- 1 | apiVersion: batch/v1 2 | kind: Job 3 | metadata: 4 | name: kubernetes-job-example 5 | labels: 6 | jobgroup: jobexample 7 | spec: 8 | template: 9 | metadata: 10 | name: kubejob 11 | labels: 12 | jobgroup: jobexample 13 | spec: 14 | containers: 15 | - name: c 16 | image: devopscube/kubernetes-job-demo:latest 17 | args: ["100"] 18 | restartPolicy: OnFailure -------------------------------------------------------------------------------- /K8sCat/Assets.xcassets/aliyun.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "aliyun.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "aliyun@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "aliyun@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /K8sCat/Assets.xcassets/config.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "config.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "config@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "config@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /K8sCat/K8sCatApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // K8sCatApp.swift 3 | // K8sCat 4 | // 5 | // Created by 顾艳华 on 2022/12/17. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct K8sCatApp: App { 12 | let persistenceController = PersistenceController.shared 13 | var body: some Scene { 14 | WindowGroup { 15 | ContentView(viewModel: ViewModel(viewContext: persistenceController.container.viewContext)) 16 | .environment(\.managedObjectContext, persistenceController.container.viewContext) 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /K8sCat/Model.xcdatamodeld/Model.xcdatamodel/contents: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /K8sCat/yaml/asset-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": { 3 | "main.css": "/static/css/main.a6b6c53c.css", 4 | "main.js": "/static/js/main.e481028a.js", 5 | "static/js/787.eb3a1d75.chunk.js": "/static/js/787.eb3a1d75.chunk.js", 6 | "index.html": "/index.html", 7 | "main.a6b6c53c.css.map": "/static/css/main.a6b6c53c.css.map", 8 | "main.e481028a.js.map": "/static/js/main.e481028a.js.map", 9 | "787.eb3a1d75.chunk.js.map": "/static/js/787.eb3a1d75.chunk.js.map" 10 | }, 11 | "entrypoints": [ 12 | "static/css/main.a6b6c53c.css", 13 | "static/js/main.e481028a.js" 14 | ] 15 | } -------------------------------------------------------------------------------- /K8sCat/yaml/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /K8sCat/yaml/index.html: -------------------------------------------------------------------------------- 1 | React App
2 | -------------------------------------------------------------------------------- /K8sCat/Model+Scale.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Model+Scale.swift 3 | // K8sCat 4 | // 5 | // Created by 顾艳华 on 2023/1/4. 6 | // 7 | 8 | import Foundation 9 | import SwiftkubeModel 10 | 11 | extension Model { 12 | func scaleDeployment(deployment: Deployment, replicas: Int32) async { 13 | if let client = client { 14 | if var scale = try? await client.appsV1.deployments.getScale(in: .namespace(deployment.namespace), name: deployment.name) { 15 | scale.spec?.replicas = replicas 16 | let _ = try? await client.appsV1.deployments.updateScale(in: .namespace(deployment.namespace), name: deployment.name, scale: scale) 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /K8sCat/yaml/static/css/main.a6b6c53c.css: -------------------------------------------------------------------------------- 1 | #root,.playground-container,body,html{background-color:#efefef;height:100vh;margin:0;padding:0}.playground-container{display:flex;flex-direction:column;max-height:100vh}.playground-panel{display:flex;flex:1 1;flex-direction:column}.cm-outer-container{flex:1 1}.cm-editor{flex:1 1;height:100%;position:relative}.cm-scroller{bottom:0;left:0;overflow-y:auto;position:absolute!important;right:0;top:0}.cm-content{word-wrap:break-word;white-space:pre-wrap;word-break:normal}.btn-container{background:#fff}.btn-container,.btn-container-dark{align-items:center;display:flex;justify-content:center}.btn-container-dark{background:#1c1c1e}.btn{background:#fff}.btn,.btn-dark{border:hidden;color:#007aff;font-size:large;margin:15px;width:288px}.btn-dark{background:#1c1c1e} 2 | /*# sourceMappingURL=main.a6b6c53c.css.map*/ -------------------------------------------------------------------------------- /K8sCat.xcodeproj/xcuserdata/guyanhua.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | K8sCat.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | 3FFCCF88294DFF6000FE6710 16 | 17 | primary 18 | 19 | 20 | 3FFCCF99294DFF6100FE6710 21 | 22 | primary 23 | 24 | 25 | 3FFCCFA3294DFF6100FE6710 26 | 27 | primary 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /K8sCatUITests/K8sCatUITestsLaunchTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // K8sCatUITestsLaunchTests.swift 3 | // K8sCatUITests 4 | // 5 | // Created by 顾艳华 on 2022/12/17. 6 | // 7 | 8 | import XCTest 9 | 10 | final class K8sCatUITestsLaunchTests: XCTestCase { 11 | 12 | override class var runsForEachTargetApplicationUIConfiguration: Bool { 13 | true 14 | } 15 | 16 | override func setUpWithError() throws { 17 | continueAfterFailure = false 18 | } 19 | 20 | func testLaunch() throws { 21 | let app = XCUIApplication() 22 | app.launch() 23 | 24 | // Insert steps here to perform after app launch but before taking a screenshot, 25 | // such as logging into a test account or navigating somewhere in the app 26 | 27 | let attachment = XCTAttachment(screenshot: app.screenshot()) 28 | attachment.name = "Launch Screen" 29 | attachment.lifetime = .keepAlways 30 | add(attachment) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /PrivacyPolicy.md: -------------------------------------------------------------------------------- 1 | ## Privacy Policy 2 | 3 | 1. We comply with the Act on the Protection of Personal Information and related laws and regulations in World and engage in the appropriate acquisition, use and provision of personal information in accordance with the below disclosed Handling of Personal Information and internal regulations, etc. 4 | 2. We strive to prevent personal information we handle from being leaked, lost, or damaged by taking necessary and proper security measures, and we take necessary corrective measures in the event that an incident occurs. 5 | 3. We clearly prescribe rules on the handling of personal information in internal regulations and thoroughly familiarize officers and employees with such rules. Moreover, when providing personal information to contractors we manage and supervise contractors to ensure they handle personal information properly. 6 | 4. A designated office accepts and appropriately responds to complaints, consultations and requests from individuals concerned for disclosure, amendment, suspension of use, etc. of personal information we retain. 7 | -------------------------------------------------------------------------------- /K8sCat/yaml/static/js/main.e481028a.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /** 2 | * @license React 3 | * react-dom.production.min.js 4 | * 5 | * Copyright (c) Facebook, Inc. and its affiliates. 6 | * 7 | * This source code is licensed under the MIT license found in the 8 | * LICENSE file in the root directory of this source tree. 9 | */ 10 | 11 | /** 12 | * @license React 13 | * react-jsx-runtime.production.min.js 14 | * 15 | * Copyright (c) Facebook, Inc. and its affiliates. 16 | * 17 | * This source code is licensed under the MIT license found in the 18 | * LICENSE file in the root directory of this source tree. 19 | */ 20 | 21 | /** 22 | * @license React 23 | * react.production.min.js 24 | * 25 | * Copyright (c) Facebook, Inc. and its affiliates. 26 | * 27 | * This source code is licensed under the MIT license found in the 28 | * LICENSE file in the root directory of this source tree. 29 | */ 30 | 31 | /** 32 | * @license React 33 | * scheduler.production.min.js 34 | * 35 | * Copyright (c) Facebook, Inc. and its affiliates. 36 | * 37 | * This source code is licensed under the MIT license found in the 38 | * LICENSE file in the root directory of this source tree. 39 | */ 40 | -------------------------------------------------------------------------------- /K8sCat/cluster/NewClusterView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NewClusterView.swift 3 | // K8sCat 4 | // 5 | // Created by 顾艳华 on 2022/12/31. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct NewClusterView: View { 11 | @Environment(\.managedObjectContext) private var viewContext 12 | let first: Bool 13 | let type: ClusterType 14 | let close: () -> Void 15 | var body: some View { 16 | switch type { 17 | case .Aliyun: 18 | ConfigView(first: first){ 19 | close() 20 | } 21 | .environment(\.managedObjectContext, viewContext) 22 | case .AWS: 23 | AWSView(first: first){ 24 | close() 25 | } 26 | .environment(\.managedObjectContext, viewContext) 27 | case .KubeConfig: 28 | ConfigView(first: first){ 29 | close() 30 | } 31 | .environment(\.managedObjectContext, viewContext) 32 | default: EmptyView() 33 | } 34 | } 35 | } 36 | 37 | struct NewClusterView_Previews: PreviewProvider { 38 | static var previews: some View { 39 | NewClusterView(first: true, type: .Demo){} 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Kube Cat 2 | 3 | Kube Cat helps you easily manage Kubernetes clusters from your mobile device, allowing you to view cluster status and quickly resolve issues while you're on the go. Kube Cat has a lot of features and more to come!: 4 | 5 | - View the status of almost any resources. e.g. 6 | Pods, Deployments, PVs, PVCs, HPAs, Jobs, CronJobs, Nodes, Config Maps, Docker containers and so on. 7 | - Inspect logs of any containers in realtime! 8 | - View / Update almost any resources via YAML. 9 | - Support managing multiple clusters. 10 | - Supported authentication providers, Kube Config, AWS IAM Authenticator (EKS), Aliyun etc. 11 | 12 | Who need it, please compile it by yourself. If you find it troublesome or want to support me, go to https://apps.apple.com/us/app/kube-cat/id1665033555 , the price is 1.99 Dollar. 13 | 14 | ![Apple iPhone 11 Screenshot 1](https://p.ipic.vip/j9wyq6.png) ![Apple iPhone 11 Screenshot 2](https://p.ipic.vip/70bdon.png) ![Apple iPhone 11 Screenshot 3](https://p.ipic.vip/8ej3sb.png) ![Apple iPhone 11 Screenshot 4](https://p.ipic.vip/rmfu9v.png) ![Apple iPhone 11 Screenshot 5](https://p.ipic.vip/w3kykg.png) ![Apple iPhone 11 Screenshot 6](https://p.ipic.vip/0qt66j.png) ![Apple iPhone 11 Screenshot 7](https://p.ipic.vip/deuaxh.png) 15 | -------------------------------------------------------------------------------- /K8sCat/Core.xcdatamodeld/Core.xcdatamodel/contents: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /K8sCatTests/K8sCatTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // K8sCatTests.swift 3 | // K8sCatTests 4 | // 5 | // Created by 顾艳华 on 2022/12/17. 6 | // 7 | 8 | import XCTest 9 | 10 | final class K8sCatTests: XCTestCase { 11 | 12 | override func setUpWithError() throws { 13 | // Put setup code here. This method is called before the invocation of each test method in the class. 14 | } 15 | 16 | override func tearDownWithError() throws { 17 | // Put teardown code here. This method is called after the invocation of each test method in the class. 18 | } 19 | 20 | func testExample() throws { 21 | // This is an example of a functional test case. 22 | // Use XCTAssert and related functions to verify your tests produce the correct results. 23 | // Any test you write for XCTest can be annotated as throws and async. 24 | // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. 25 | // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. 26 | } 27 | 28 | func testPerformanceExample() throws { 29 | // This is an example of a performance test case. 30 | measure { 31 | // Put the code you want to measure the time of here. 32 | } 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /K8sCat/inner/LogView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LogView.swift 3 | // K8sCat 4 | // 5 | // Created by 顾艳华 on 3/4/24. 6 | // 7 | 8 | import SwiftUI 9 | import SwiftkubeClient 10 | import SwiftkubeModel 11 | import SwiftUIX 12 | 13 | struct LogView: View { 14 | var logTask: SwiftkubeClientTask? 15 | @State var search = "" 16 | @State var logsLines: [String] = [] 17 | 18 | init(logTask: SwiftkubeClientTask?) { 19 | self.logTask = logTask 20 | } 21 | var body: some View { 22 | SearchBar(text: $search) 23 | .padding() 24 | List { 25 | ForEach(logsLines, id: \.self) { 26 | l in 27 | if l.contains(search) { 28 | Text(l) 29 | .foregroundColor(.systemYellow) 30 | } else { 31 | Text(l) 32 | } 33 | 34 | } 35 | } 36 | .onAppear { 37 | Task { 38 | if let logTask = logTask { 39 | for try await line in logTask.start() { 40 | // print(line) 41 | DispatchQueue.main.async { 42 | logsLines.append(line) 43 | } 44 | } 45 | } 46 | } 47 | } 48 | } 49 | } 50 | 51 | //#Preview { 52 | // LogView() 53 | //} 54 | -------------------------------------------------------------------------------- /K8sCatUITests/K8sCatUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // K8sCatUITests.swift 3 | // K8sCatUITests 4 | // 5 | // Created by 顾艳华 on 2022/12/17. 6 | // 7 | 8 | import XCTest 9 | 10 | final class K8sCatUITests: XCTestCase { 11 | 12 | override func setUpWithError() throws { 13 | // Put setup code here. This method is called before the invocation of each test method in the class. 14 | 15 | // In UI tests it is usually best to stop immediately when a failure occurs. 16 | continueAfterFailure = false 17 | 18 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 19 | } 20 | 21 | override func tearDownWithError() throws { 22 | // Put teardown code here. This method is called after the invocation of each test method in the class. 23 | } 24 | 25 | func testExample() throws { 26 | // UI tests must launch the application that they test. 27 | let app = XCUIApplication() 28 | app.launch() 29 | 30 | // Use XCTAssert and related functions to verify your tests produce the correct results. 31 | } 32 | 33 | func testLaunchPerformance() throws { 34 | if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { 35 | // This measures how long it takes to launch your application. 36 | measure(metrics: [XCTApplicationLaunchMetric()]) { 37 | XCUIApplication().launch() 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /K8sCat/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // K8sCat 4 | // 5 | // Created by 顾艳华 on 2022/12/17. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ContentView: View { 11 | @ObservedObject var viewModel: ViewModel 12 | @Environment(\.managedObjectContext) private var viewContext 13 | var body: some View { 14 | TabView { 15 | NamespaceView(viewModel: viewModel) 16 | .environment(\.managedObjectContext, viewContext) 17 | .tabItem { 18 | Label("Payloads", systemImage: "aqi.medium") 19 | } 20 | 21 | StorageView(viewModel: viewModel) 22 | .environment(\.managedObjectContext, viewContext) 23 | .tabItem { 24 | Label("Storage", systemImage: "opticaldiscdrive") 25 | } 26 | 27 | GlobalView(viewModel: viewModel) 28 | .environment(\.managedObjectContext, viewContext) 29 | .tabItem { 30 | Label("Nodes", systemImage: "display.2") 31 | } 32 | 33 | SettingView(viewModel: viewModel) 34 | .environment(\.managedObjectContext, viewContext) 35 | .tabItem { 36 | Label("Setting", systemImage: "gear") 37 | } 38 | } 39 | } 40 | } 41 | 42 | struct ContentView_Previews: PreviewProvider { 43 | static var previews: some View { 44 | ContentView(viewModel: ViewModel(viewContext: PersistenceController.preview.container.viewContext)) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /K8sCat.xcodeproj/project.xcworkspace/xcuserdata/guyanhua.xcuserdatad/xcdebugger/Expressions.xcexplist: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 7 | 8 | 10 | 11 | 12 | 13 | 15 | 16 | 18 | 19 | 20 | 21 | 23 | 24 | 26 | 27 | 29 | 30 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /K8sCat/inner/ExecWebView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WebView.swift 3 | // Yamler 4 | // 5 | // Created by 顾艳华 on 2022/12/14. 6 | // 7 | 8 | import SwiftUI 9 | import WebKit 10 | import Foundation 11 | /* 12 | TODO 13 | */ 14 | struct ExecWebView: UIViewRepresentable { 15 | 16 | @Environment(\.colorScheme) private var colorScheme 17 | // let pod: Pod 18 | // let container: Container 19 | let close: () -> Void 20 | 21 | func makeUIView(context: Context) -> WKWebView { 22 | let webView = WKWebView() 23 | 24 | let baseUrl = Bundle.main.url(forResource: "index", withExtension: "html", subdirectory: "exec")! 25 | // print(baseUrl) 26 | var component = URLComponents(url: baseUrl, resolvingAgainstBaseURL: false) 27 | component?.queryItems = [URLQueryItem(name: "theme", value: colorScheme == .dark ? "dark" : "light")] 28 | 29 | if let url = component?.url { 30 | // print(url) 31 | webView.loadFileURL(url, allowingReadAccessTo: url) 32 | } 33 | return webView 34 | } 35 | 36 | func updateUIView(_ webView: WKWebView, context: Context) { 37 | 38 | 39 | 40 | 41 | // let baseUrl = URL(string: "http://localhost:3000")! 42 | // var component = URLComponents(url: baseUrl, resolvingAgainstBaseURL: false) 43 | // component?.queryItems = [URLQueryItem(name: "items", value: string)] 44 | // if let url = component?.url { 45 | // print(url) 46 | // webView.load(URLRequest(url: url)) 47 | // } 48 | 49 | } 50 | } 51 | 52 | -------------------------------------------------------------------------------- /K8sCat/yaml/static/css/main.a6b6c53c.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"static/css/main.a6b6c53c.css","mappings":"AAAA,sCAOE,wBAAyB,CADzB,YAAa,CADb,QAAS,CADT,SAIF,CAEA,sBACE,YAAa,CACb,qBAAsB,CACtB,gBACF,CAEA,kBAEE,YAAa,CADb,QAAO,CAEP,qBACF,CAEA,oBACE,QACF,CAEA,WACE,QAAO,CACP,WAAY,CACZ,iBAEF,CAEA,aAIE,QAAS,CACT,MAAO,CACP,eAAgB,CALhB,2BAA6B,CAE7B,OAAQ,CADR,KAKF,CAEA,YACE,oBAAqB,CACrB,oBAAqB,CACrB,iBACF,CAEA,eAIE,eACF,CAEA,mCALE,kBAAmB,CADnB,YAAa,CAEb,sBASF,CALA,oBAIE,kBACF,CAEA,KAKE,eAEF,CAEA,eANE,aAAc,CACd,aAAc,CAEd,eAAgB,CALhB,WAAY,CACZ,WAcF,CAPA,UAKE,kBAEF","sources":["App.css"],"sourcesContent":["html,\nbody,\n#root,\n.playground-container {\n padding: 0;\n margin: 0;\n height: 100vh;\n background-color: #efefef;\n}\n\n.playground-container {\n display: flex;\n flex-direction: column;\n max-height: 100vh;\n}\n\n.playground-panel {\n flex: 1;\n display: flex;\n flex-direction: column;\n}\n\n.cm-outer-container {\n flex: 1;\n}\n\n.cm-editor {\n flex: 1;\n height: 100%;\n position: relative;\n /* <-- .cm-scroller absolute positioning depends on this*/\n}\n\n.cm-scroller {\n position: absolute !important;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n overflow-y: auto;\n}\n\n.cm-content {\n word-wrap: break-word;\n white-space: pre-wrap;\n word-break: normal;\n}\n\n.btn-container {\n display: flex;\n align-items: center;\n justify-content: center;\n background: white;\n}\n\n.btn-container-dark {\n display: flex;\n align-items: center;\n justify-content: center;\n background: #1C1C1E;\n}\n\n.btn {\n margin: 15px;\n width: 288px;\n border: hidden;\n color: #007AFF;\n background: white;\n font-size: large;\n}\n\n.btn-dark {\n margin: 15px;\n width: 288px;\n border: hidden;\n color: #007AFF;\n background: #1C1C1E;\n font-size: large;\n}"],"names":[],"sourceRoot":""} -------------------------------------------------------------------------------- /K8sCat/SettingView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingView.swift 3 | // K8sCat 4 | // 5 | // Created by 顾艳华 on 2022/12/20. 6 | // 7 | 8 | import SwiftUI 9 | import StoreKit 10 | 11 | struct SettingView: View { 12 | let viewModel: ViewModel 13 | @Environment(\.managedObjectContext) private var viewContext 14 | @Environment(\.requestReview) var requestReview 15 | 16 | var body: some View { 17 | NavigationStack { 18 | Form{ 19 | Section(){ 20 | Button{ 21 | #if os(iOS) 22 | if let url = URL(string: "https://github.com/buhe/KubeCat/issues") { 23 | UIApplication.shared.open(url) 24 | } 25 | #endif 26 | } label: { 27 | 28 | Text("Feedback") 29 | 30 | }.buttonStyle(PlainButtonStyle()) 31 | Button{ 32 | requestReview() 33 | } label: { 34 | 35 | Text("Rate") 36 | 37 | }.buttonStyle(PlainButtonStyle()) 38 | HStack{ 39 | Text("Version") 40 | Spacer() 41 | Text(Bundle.main.releaseVersionNumber ?? "") 42 | } 43 | HStack{ 44 | Text("License") 45 | Spacer() 46 | Text("GPLv3") 47 | } 48 | } 49 | 50 | 51 | } 52 | } 53 | } 54 | } 55 | 56 | struct SettingView_Previews: PreviewProvider { 57 | static var previews: some View { 58 | SettingView(viewModel: ViewModel(viewContext: PersistenceController.preview.container.viewContext)) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /K8sCat/inner/PVCView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PVCView.swift 3 | // K8sCat 4 | // 5 | // Created by 顾艳华 on 2022/12/29. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct PVCView: View { 11 | let pvc: PersistentVolumeClaim 12 | let viewModel: ViewModel 13 | 14 | // @State var showYaml = false 15 | var body: some View { 16 | Form { 17 | Section(header: "Name") { 18 | Text(pvc.name) 19 | } 20 | 21 | 22 | } 23 | .navigationTitle("Persistent Volume Claim") 24 | // .toolbar{ 25 | // Menu { 26 | // Button { 27 | // // do something 28 | // let yaml = pvc.encodeYaml(client: viewModel.model.client) 29 | // print("Yaml: \(yaml)") 30 | // showYaml = true 31 | //// deployment.decodeYaml(client: viewModel.model.client, yaml: yaml) 32 | // } label: { 33 | // Text("View/Edit Yaml") 34 | // Image(systemName: "note.text") 35 | // } 36 | // Button { 37 | // // do something 38 | // } label: { 39 | // Text("Delete Resource") 40 | // Image(systemName: "trash") 41 | // } 42 | // } label: { 43 | // Image(systemName: "ellipsis") 44 | // } 45 | // }.sheet(isPresented: $showYaml){ 46 | // YamlWebView(yamlble: pvc, model: viewModel.model) { 47 | // showYaml = false 48 | // } 49 | // Button{ 50 | // urlScheme(yamlble: pvc, client: viewModel.model.client) 51 | // }label: { 52 | // Text("Load yaml via Yamler") 53 | // }.padding() 54 | // } 55 | } 56 | } 57 | 58 | //struct PVCView_Previews: PreviewProvider { 59 | // static var previews: some View { 60 | // PVCView() 61 | // } 62 | //} 63 | -------------------------------------------------------------------------------- /K8sCat/Persistence.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Persistence.swift 3 | // Convict Conditioning Pro 4 | // 5 | // Created by 顾艳华 on 2022/12/23. 6 | // 7 | 8 | import CoreData 9 | 10 | struct PersistenceController { 11 | static let shared = PersistenceController() 12 | 13 | static var preview: PersistenceController = { 14 | let result = PersistenceController(inMemory: true) 15 | let viewContext = result.container.viewContext 16 | for _ in 0..<10 { 17 | let newItem = ClusterEntry(context: viewContext) 18 | newItem.config = """ 19 | 20 | """ 21 | } 22 | do { 23 | try viewContext.save() 24 | } catch { 25 | // Replace this implementation with code to handle the error appropriately. 26 | // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. 27 | let nsError = error as NSError 28 | fatalError("Unresolved error \(nsError), \(nsError.userInfo)") 29 | } 30 | return result 31 | }() 32 | 33 | let container: NSPersistentCloudKitContainer 34 | 35 | init(inMemory: Bool = false) { 36 | container = NSPersistentCloudKitContainer(name: "Core") 37 | if inMemory { 38 | container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null") 39 | } 40 | container.loadPersistentStores(completionHandler: { (storeDescription, error) in 41 | if let error = error as NSError? { 42 | // Replace this implementation with code to handle the error appropriately. 43 | // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. 44 | 45 | /* 46 | Typical reasons for an error here include: 47 | * The parent directory does not exist, cannot be created, or disallows writing. 48 | * The persistent store is not accessible, due to permissions or data protection when the device is locked. 49 | * The device is out of space. 50 | * The store could not be migrated to the current model version. 51 | Check the error message to determine what the actual problem was. 52 | */ 53 | fatalError("Unresolved error \(error), \(error.userInfo)") 54 | } 55 | }) 56 | container.viewContext.automaticallyMergesChangesFromParent = true 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /K8sCat/inner/YamlWebView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WebView.swift 3 | // Yamler 4 | // 5 | // Created by 顾艳华 on 2022/12/14. 6 | // 7 | 8 | import SwiftUI 9 | import WebKit 10 | import Foundation 11 | 12 | class WKHandler: NSObject, WKScriptMessageHandler { 13 | let yamlble: Yamlble 14 | let model: Model 15 | var lastYaml: String 16 | let close: () -> Void 17 | init(yamlble: Yamlble, model: Model, lastYaml: String, close: @escaping () -> Void) { 18 | self.yamlble = yamlble 19 | self.model = model 20 | self.lastYaml = lastYaml 21 | self.close = close 22 | } 23 | func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { 24 | guard let dict = message.body as? [String : AnyObject] else { 25 | return 26 | } 27 | 28 | print("from webview \(dict)") 29 | if dict["message"] != nil { 30 | lastYaml = dict["message"]! as! String 31 | } 32 | if dict["done"] != nil && dict["done"]! as! String == "true" { 33 | // download yaml 34 | print("load via yaml \(lastYaml)") 35 | Task { 36 | await yamlble.decodeYamlAndUpdate(client: model.client, yaml: lastYaml) 37 | } 38 | close() 39 | } 40 | } 41 | } 42 | struct YamlWebView: UIViewRepresentable { 43 | 44 | @Environment(\.colorScheme) private var colorScheme 45 | // let pod: Pod 46 | // let container: Container 47 | let yamlble: Yamlble 48 | let model: Model 49 | let close: () -> Void 50 | 51 | func makeUIView(context: Context) -> WKWebView { 52 | let webView = WKWebView() 53 | Task { 54 | let yaml = await yamlble.encodeYaml(client: model.client) 55 | webView.configuration.userContentController.add(WKHandler(yamlble: yamlble, model: model, lastYaml: yaml, close: close), name: "toggleMessageHandler") 56 | if let baseUrl = Bundle.main.url(forResource: "index", withExtension: "html", subdirectory: "yaml") { 57 | // print(baseUrl) 58 | var component = URLComponents(url: baseUrl, resolvingAgainstBaseURL: false) 59 | component?.queryItems = [URLQueryItem(name: "items", value: yaml), URLQueryItem(name: "theme", value: colorScheme == .dark ? "dark" : "light")] 60 | 61 | if let url = component?.url { 62 | print(url) 63 | webView.loadFileURL(url, allowingReadAccessTo: url) 64 | } 65 | 66 | } 67 | } 68 | return webView 69 | } 70 | 71 | func updateUIView(_ webView: WKWebView, context: Context) { 72 | 73 | } 74 | } 75 | 76 | -------------------------------------------------------------------------------- /K8sCat/cluster/ClusterTypeView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ClusterTypeView.swift 3 | // K8sCat 4 | // 5 | // Created by 顾艳华 on 2022/12/31. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ClusterTypeView: View { 11 | @Environment(\.managedObjectContext) private var viewContext 12 | @Environment(\.presentationMode) var presentationMode 13 | let first: Bool 14 | let close: () -> Void 15 | var body: some View { 16 | NavigationStack { 17 | List { 18 | ForEach(ClusterType.allCases, id: \.self) { 19 | c in 20 | switch c { 21 | case .KubeConfig: 22 | NavigationLink { 23 | NewClusterView(first: first, type: c){ 24 | presentationMode.wrappedValue.dismiss() 25 | close() 26 | } 27 | .environment(\.managedObjectContext, viewContext) 28 | } label: { 29 | Image("config") 30 | Text(c.rawValue) 31 | 32 | 33 | } 34 | case .Aliyun: 35 | NavigationLink { 36 | NewClusterView(first: first, type: c){ 37 | presentationMode.wrappedValue.dismiss() 38 | close() 39 | } 40 | .environment(\.managedObjectContext, viewContext) 41 | } label: { 42 | Image("aliyun") 43 | Text(c.rawValue) 44 | 45 | 46 | } 47 | case .AWS: 48 | NavigationLink { 49 | NewClusterView(first: first, type: c){ 50 | presentationMode.wrappedValue.dismiss() 51 | close() 52 | } 53 | .environment(\.managedObjectContext, viewContext) 54 | } label: { 55 | Image("aws") 56 | Text(c.rawValue) 57 | 58 | 59 | } 60 | default: EmptyView() 61 | } 62 | 63 | } 64 | } 65 | } 66 | } 67 | } 68 | 69 | struct ClusterTypeView_Previews: PreviewProvider { 70 | static var previews: some View { 71 | ClusterTypeView(first: true){} 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /K8sCat/GlobalView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GlobalView.swift 3 | // K8sCat 4 | // 5 | // Created by 顾艳华 on 2022/12/20. 6 | // 7 | 8 | import SwiftUI 9 | import SwiftUIX 10 | 11 | struct GlobalView: View { 12 | @State var search = "" 13 | @State var tabIndex = 0 14 | @ObservedObject var viewModel: ViewModel 15 | @State var showCluster = false 16 | @FetchRequest( 17 | sortDescriptors: [], 18 | animation: .default) 19 | private var cluters: FetchedResults 20 | @Environment(\.managedObjectContext) private var viewContext 21 | 22 | var body: some View { 23 | VStack { 24 | NavigationStack { 25 | HStack{ 26 | SearchBar(text: $search).padding(.horizontal) 27 | Button{ showCluster = true }label: { 28 | Image(systemName: cluters.filter{$0.selected}.first?.icon ?? "0.circle") 29 | }.padding(.trailing) 30 | } 31 | NodesTabBar(tabIndex: $tabIndex).padding(.horizontal, 26) 32 | switch tabIndex { 33 | case 0: 34 | List { 35 | ForEach(viewModel.nodes.filter{$0.name.contains(search.lowercased()) || search == ""}) { 36 | i in 37 | NavigationLink { 38 | NodeView(node: i, viewModel: viewModel) 39 | } label: { 40 | Image(systemName: "display") 41 | VStack(alignment: .leading) { 42 | Text(i.name) 43 | CaptionText(text: i.version) 44 | } 45 | } 46 | 47 | } 48 | }.listStyle(PlainListStyle()) 49 | .onAppear { 50 | Task { 51 | await viewModel.nodes() 52 | } 53 | } 54 | .refreshable { 55 | viewModel.model.nodes = nil 56 | Task { 57 | await viewModel.nodes() 58 | } 59 | } 60 | default: 61 | EmptyView() 62 | } 63 | } 64 | }.sheet(isPresented: $showCluster){ 65 | ClusterView(viewModel: viewModel){ 66 | showCluster = false 67 | } 68 | .environment(\.managedObjectContext, viewContext) 69 | } 70 | } 71 | } 72 | 73 | struct GlobalView_Previews: PreviewProvider { 74 | static var previews: some View { 75 | GlobalView(viewModel: ViewModel(viewContext: PersistenceController.preview.container.viewContext)) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /K8sCat/inner/ContainerView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContainerView.swift 3 | // K8sCat 4 | // 5 | // Created by 顾艳华 on 2022/12/26. 6 | // 7 | 8 | import SwiftUI 9 | import SwiftkubeClient 10 | import SwiftkubeModel 11 | import SwiftUIX 12 | 13 | struct ContainerView: View { 14 | let pod: Pod 15 | let container: Container 16 | let viewModel: ViewModel 17 | 18 | @State var showLogs = false 19 | @State var showShell = false 20 | 21 | 22 | func logs() -> SwiftkubeClientTask? { 23 | viewModel.model.logs(in: .namespace(viewModel.ns), pod: pod, container: container) 24 | } 25 | 26 | var body: some View { 27 | Form { 28 | Section(header: "Name") { 29 | Text(container.name) 30 | } 31 | Section(header: "Status") { 32 | Text(container.status.rawValue) 33 | } 34 | Section(header: "Error") { 35 | Text(container.error ? "True" : "False") 36 | .foregroundColor(container.error ? .red : .none) 37 | } 38 | Section(header: "Ready") { 39 | Text(container.ready ? "True" : "False") 40 | .foregroundColor(container.ready ? .none : .yellow) 41 | } 42 | Section(header: "Image") { 43 | Text(container.image).font(.caption) 44 | } 45 | Section(header: "Termination Message") { 46 | HStack{ 47 | Text("Path") 48 | Spacer() 49 | Text(container.path) 50 | } 51 | HStack{ 52 | Text("Policy") 53 | Spacer() 54 | Text(container.policy) 55 | } 56 | } 57 | 58 | Section(header: "Misc") { 59 | HStack{ 60 | Text("Pull Policy") 61 | Spacer() 62 | Text(container.pullPolicy) 63 | } 64 | 65 | } 66 | } 67 | .toolbar { 68 | // Button { 69 | // showShell = true 70 | // } label: { 71 | // Label("shell", systemImage: "terminal") 72 | // } 73 | 74 | Button { 75 | viewModel.logTask = logs() 76 | showLogs = true 77 | 78 | } label: { 79 | Label("log", systemImage: "doc.text.magnifyingglass") 80 | } 81 | }.sheet(isPresented: $showLogs, onDismiss: { 82 | viewModel.logTask?.cancel() 83 | }) { 84 | LogView(logTask: viewModel.logTask) 85 | } 86 | .sheet(isPresented: $showShell) { 87 | // WebView(pod: pod, container: container).padding() 88 | } 89 | .navigationTitle("Container") 90 | } 91 | } 92 | 93 | //struct ContainerView_Previews: PreviewProvider { 94 | // static var previews: some View { 95 | // ContainerView() 96 | // } 97 | //} 98 | 99 | enum ContainerStatus: String { 100 | case Running 101 | case Waiting 102 | case Terminated 103 | } 104 | -------------------------------------------------------------------------------- /K8sCat/inner/SecretView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SecretView.swift 3 | // K8sCat 4 | // 5 | // Created by 顾艳华 on 2022/12/26. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct SecretView: View { 11 | let secret: Secret 12 | let viewModel: ViewModel 13 | var body: some View { 14 | Form { 15 | Section(header: "Name") { 16 | Text(secret.name) 17 | } 18 | Section(header: "Data") { 19 | 20 | List { 21 | ForEach((secret.data ?? [:]).sorted(by: >), id: \.key) { 22 | key, value in 23 | NavigationLink { 24 | Form{ 25 | Section(header: "Name"){ 26 | Text(key) 27 | } 28 | Section(header: "Data"){ 29 | Text(value) 30 | } 31 | 32 | 33 | } 34 | } label: { 35 | VStack(alignment: .leading) { 36 | Text(key) 37 | } 38 | } 39 | } 40 | } 41 | 42 | 43 | 44 | } 45 | Section(header: "Labels and Annotations") { 46 | NavigationLink { 47 | List { 48 | ForEach((secret.labels ?? [:]).sorted(by: >), id: \.key) { 49 | key, value in 50 | VStack(alignment: .leading) { 51 | Text(key) 52 | 53 | CaptionText(text: value) 54 | } 55 | } 56 | } 57 | 58 | } label: { 59 | Text("Labels") 60 | } 61 | NavigationLink { 62 | List { 63 | ForEach((secret.annotations ?? [:]).sorted(by: >), id: \.key) { 64 | key, value in 65 | VStack(alignment: .leading) { 66 | Text(key) 67 | CaptionText(text: value) 68 | } 69 | } 70 | } 71 | } label: { 72 | Text("Annotations") 73 | } 74 | } 75 | 76 | Section(header: "Misc") { 77 | HStack{ 78 | Text("Namespace") 79 | Spacer() 80 | Text(secret.namespace) 81 | } 82 | 83 | } 84 | } 85 | .navigationTitle("Secret") 86 | } 87 | } 88 | 89 | //struct SecretView_Previews: PreviewProvider { 90 | // static var previews: some View { 91 | // SecretView(secret: Secret(id: "abc", name: "abc", labels: ["l1":"l1v"], annotations: ["l1":"l1v"], namespace: "default", data: ["l1":"l1v"]), viewModel: ViewModel(viewContext: PersistenceController.preview.container.viewContext)) 92 | // } 93 | //} 94 | -------------------------------------------------------------------------------- /K8sCat/inner/ConfigMapView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConfigMapView.swift 3 | // K8sCat 4 | // 5 | // Created by 顾艳华 on 2022/12/26. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ConfigMapView: View { 11 | let configMap: ConfigMap 12 | let viewModel: ViewModel 13 | 14 | var body: some View { 15 | Form { 16 | Section(header: "Name") { 17 | Text(configMap.name) 18 | } 19 | Section(header: "Data") { 20 | 21 | List { 22 | ForEach((configMap.data ?? [:]).sorted(by: >), id: \.key) { 23 | key, value in 24 | NavigationLink { 25 | Form{ 26 | Section(header: "Name"){ 27 | Text(key) 28 | } 29 | Section(header: "Data"){ 30 | Text(value) 31 | } 32 | 33 | 34 | } 35 | } label: { 36 | VStack(alignment: .leading) { 37 | Text(key) 38 | } 39 | } 40 | } 41 | } 42 | 43 | 44 | 45 | } 46 | Section(header: "Labels and Annotations") { 47 | NavigationLink { 48 | List { 49 | ForEach((configMap.labels ?? [:]).sorted(by: >), id: \.key) { 50 | key, value in 51 | VStack(alignment: .leading) { 52 | Text(key) 53 | 54 | CaptionText(text: value) 55 | } 56 | } 57 | } 58 | 59 | } label: { 60 | Text("Labels") 61 | } 62 | NavigationLink { 63 | List { 64 | ForEach((configMap.annotations ?? [:]).sorted(by: >), id: \.key) { 65 | key, value in 66 | VStack(alignment: .leading) { 67 | Text(key) 68 | CaptionText(text: value) 69 | } 70 | } 71 | } 72 | } label: { 73 | Text("Annotations") 74 | } 75 | } 76 | 77 | Section(header: "Misc") { 78 | HStack{ 79 | Text("Namespace") 80 | Spacer() 81 | Text(configMap.namespace) 82 | } 83 | 84 | } 85 | } 86 | .navigationTitle("Config Map") 87 | } 88 | } 89 | 90 | struct ConfigMapView_Previews: PreviewProvider { 91 | static var previews: some View { 92 | ConfigMapView(configMap: ConfigMap(id: "abc", name: "abc", labels: ["l1":"l1v"], annotations: ["l1":"l1v"], namespace: "default", data: ["l1":"l1v"]), viewModel: ViewModel(viewContext: PersistenceController.preview.container.viewContext)) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /K8sCat/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images": [ 3 | { 4 | "size": "20x20", 5 | "idiom": "iphone", 6 | "filename": "icon-20@2x.png", 7 | "scale": "2x" 8 | }, 9 | { 10 | "size": "20x20", 11 | "idiom": "iphone", 12 | "filename": "icon-20@3x.png", 13 | "scale": "3x" 14 | }, 15 | { 16 | "size": "29x29", 17 | "idiom": "iphone", 18 | "filename": "icon-29.png", 19 | "scale": "1x" 20 | }, 21 | { 22 | "size": "29x29", 23 | "idiom": "iphone", 24 | "filename": "icon-29@2x.png", 25 | "scale": "2x" 26 | }, 27 | { 28 | "size": "29x29", 29 | "idiom": "iphone", 30 | "filename": "icon-29@3x.png", 31 | "scale": "3x" 32 | }, 33 | { 34 | "size": "40x40", 35 | "idiom": "iphone", 36 | "filename": "icon-40@2x.png", 37 | "scale": "2x" 38 | }, 39 | { 40 | "size": "40x40", 41 | "idiom": "iphone", 42 | "filename": "icon-40@3x.png", 43 | "scale": "3x" 44 | }, 45 | { 46 | "size": "60x60", 47 | "idiom": "iphone", 48 | "filename": "icon-60@2x.png", 49 | "scale": "2x" 50 | }, 51 | { 52 | "size": "60x60", 53 | "idiom": "iphone", 54 | "filename": "icon-60@3x.png", 55 | "scale": "3x" 56 | }, 57 | { 58 | "size": "20x20", 59 | "idiom": "ipad", 60 | "filename": "icon-20-ipad.png", 61 | "scale": "1x" 62 | }, 63 | { 64 | "size": "20x20", 65 | "idiom": "ipad", 66 | "filename": "icon-20@2x-ipad.png", 67 | "scale": "2x" 68 | }, 69 | { 70 | "size": "29x29", 71 | "idiom": "ipad", 72 | "filename": "icon-29-ipad.png", 73 | "scale": "1x" 74 | }, 75 | { 76 | "size": "29x29", 77 | "idiom": "ipad", 78 | "filename": "icon-29@2x-ipad.png", 79 | "scale": "2x" 80 | }, 81 | { 82 | "size": "40x40", 83 | "idiom": "ipad", 84 | "filename": "icon-40.png", 85 | "scale": "1x" 86 | }, 87 | { 88 | "size": "40x40", 89 | "idiom": "ipad", 90 | "filename": "icon-40@2x.png", 91 | "scale": "2x" 92 | }, 93 | { 94 | "size": "76x76", 95 | "idiom": "ipad", 96 | "filename": "icon-76.png", 97 | "scale": "1x" 98 | }, 99 | { 100 | "size": "76x76", 101 | "idiom": "ipad", 102 | "filename": "icon-76@2x.png", 103 | "scale": "2x" 104 | }, 105 | { 106 | "size": "83.5x83.5", 107 | "idiom": "ipad", 108 | "filename": "icon-83.5@2x.png", 109 | "scale": "2x" 110 | }, 111 | { 112 | "size": "1024x1024", 113 | "idiom": "ios-marketing", 114 | "filename": "icon-1024.png", 115 | "scale": "1x" 116 | } 117 | ], 118 | "info": { 119 | "version": 1, 120 | "author": "icon.wuruihong.com" 121 | } 122 | } -------------------------------------------------------------------------------- /K8sCat/inner/HpaView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HpaView.swift 3 | // K8sCat 4 | // 5 | // Created by 顾艳华 on 2023/1/2. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct HpaView: View { 11 | let hpa: Hpa 12 | let viewModel: ViewModel 13 | 14 | @State var deployment: Deployment? 15 | @State var showYaml = false 16 | var body: some View { 17 | Form { 18 | Section(header: "Name") { 19 | Text(hpa.name) 20 | } 21 | Section(header: "Reference") { 22 | NavigationLink { 23 | switch hpa.referenceType { 24 | case .Deployment: 25 | DeploymentView(deployment: deployment, viewModel: viewModel) 26 | .onAppear { 27 | Task { 28 | self.deployment = await viewModel.model.deploymentByName(ns: viewModel.ns, name: hpa.reference) 29 | } 30 | } 31 | default: 32 | EmptyView() 33 | } 34 | } label: { 35 | 36 | switch hpa.referenceType { 37 | case .Deployment: 38 | Image(systemName: "ipad.landscape.badge.play") 39 | default: 40 | EmptyView() 41 | } 42 | Text(hpa.reference) 43 | } 44 | } 45 | Section(header: "Misc") { 46 | HStack{ 47 | Text("Namespace") 48 | Spacer() 49 | Text(hpa.namespace) 50 | } 51 | 52 | } 53 | }.toolbar{ 54 | Menu { 55 | Button { 56 | // do something 57 | // let yaml = hpa.encodeYaml(client: viewModel.model.client) 58 | // print("Yaml: \(yaml)") 59 | showYaml = true 60 | // deployment.decodeYaml(client: viewModel.model.client, yaml: yaml) 61 | } label: { 62 | Text("View/Edit Yaml") 63 | Image(systemName: "note.text") 64 | } 65 | // Button { 66 | // // do something 67 | // } label: { 68 | // Text("Delete Resource") 69 | // Image(systemName: "trash") 70 | // } 71 | } label: { 72 | Image(systemName: "ellipsis") 73 | } 74 | }.sheet(isPresented: $showYaml){ 75 | #if os(iOS) 76 | YamlWebView(yamlble: hpa, model: viewModel.model) { 77 | showYaml = false 78 | } 79 | #endif 80 | Button{ 81 | Task { 82 | await urlScheme(yamlble: hpa, client: viewModel.model.client) 83 | } 84 | }label: { 85 | Text("Load yaml via Yamler") 86 | }.padding() 87 | } 88 | .navigationTitle("‎Horizontal Pod Autoscaler") 89 | } 90 | } 91 | 92 | struct HpaView_Previews: PreviewProvider { 93 | static var previews: some View { 94 | HpaView(hpa: Hpa(id: "123", name: "123", namespace: "default",reference: "123", referenceType: .Deployment, raw: nil), viewModel: ViewModel(viewContext: PersistenceController.shared.container.viewContext)) 95 | } 96 | } 97 | 98 | enum HPAReference: String { 99 | case Deployment 100 | case UnKnow 101 | } 102 | -------------------------------------------------------------------------------- /K8sCat/inner/PVView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PVView.swift 3 | // K8sCat 4 | // 5 | // Created by 顾艳华 on 2022/12/29. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct PVView: View { 11 | let pv: PersistentVolume 12 | let viewModel: ViewModel 13 | 14 | @State var showYaml = false 15 | var body: some View { 16 | Form { 17 | Section(header: "Name") { 18 | Text(pv.name) 19 | } 20 | Section(header: "Status") { 21 | Text(pv.status) 22 | } 23 | Section(header: "Access Modes") { 24 | Text(pv.accessModes) 25 | } 26 | Section(header: "Storage Class") { 27 | Text(pv.storageClass) 28 | } 29 | Section(header: "Labels and Annotations") { 30 | NavigationLink { 31 | List { 32 | ForEach((pv.labels ?? [:]).sorted(by: >), id: \.key) { 33 | key, value in 34 | VStack(alignment: .leading) { 35 | Text(key) 36 | 37 | CaptionText(text: value) 38 | } 39 | } 40 | } 41 | 42 | } label: { 43 | Text("Labels") 44 | } 45 | NavigationLink { 46 | List { 47 | ForEach((pv.annotations ?? [:]).sorted(by: >), id: \.key) { 48 | key, value in 49 | VStack(alignment: .leading) { 50 | Text(key) 51 | CaptionText(text: value) 52 | } 53 | } 54 | } 55 | } label: { 56 | Text("Annotations") 57 | } 58 | } 59 | 60 | }.toolbar{ 61 | Menu { 62 | Button { 63 | // do something 64 | let yaml = pv.encodeYaml(client: viewModel.model.client) 65 | print("Yaml: \(yaml)") 66 | showYaml = true 67 | // deployment.decodeYaml(client: viewModel.model.client, yaml: yaml) 68 | } label: { 69 | Text("View/Edit Yaml") 70 | Image(systemName: "note.text") 71 | } 72 | // Button { 73 | // // do something 74 | // } label: { 75 | // Text("Delete Resource") 76 | // Image(systemName: "trash") 77 | // } 78 | } label: { 79 | Image(systemName: "ellipsis") 80 | } 81 | }.sheet(isPresented: $showYaml){ 82 | #if os(iOS) 83 | YamlWebView(yamlble: pv, model: viewModel.model) { 84 | showYaml = false 85 | } 86 | #endif 87 | Button { 88 | Task { 89 | await urlScheme(yamlble: pv, client: viewModel.model.client) 90 | } 91 | }label: { 92 | Text("Load yaml via Yamler") 93 | }.padding() 94 | } 95 | .navigationTitle("Persistent Volume") 96 | } 97 | } 98 | 99 | //struct PVView_Previews: PreviewProvider { 100 | // static var previews: some View { 101 | // PVView(pv: PersistentVolume(id: "123", name: "123", labels: ["l1":"l1v"], annotations: ["l1":"l1v"],accessModes: "r/w",status: "B",storageClass: "M"), viewModel: ViewModel(viewContext: PersistenceController.preview.container.viewContext)) 102 | // } 103 | //} 104 | -------------------------------------------------------------------------------- /K8sCat/inner/JobView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JobView.swift 3 | // K8sCat 4 | // 5 | // Created by 顾艳华 on 2022/12/26. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct JobView: View { 11 | @State var pods: [Pod] = [] 12 | let job: Job 13 | let viewModel: ViewModel 14 | var body: some View { 15 | Form { 16 | Section(header: "Name") { 17 | Text(job.name) 18 | } 19 | Section(header: "Status") { 20 | Text(job.status ? "Succeeded" : "Failed") 21 | } 22 | Section(header: "Pods") { 23 | List { 24 | ForEach(pods) { 25 | i in 26 | NavigationLink { 27 | PodView(pod: i, viewModel: viewModel) 28 | } label: { 29 | Image(systemName: "tray.2") 30 | VStack(alignment: .leading) { 31 | Text(i.name) 32 | HStack{ 33 | Text("containers -").font(.caption) 34 | CaptionText(text: "expect: \(i.expect), ") 35 | Text("error: \(i.status == PodStatus.Succeeded.rawValue ? 0 : i.error)").font(.caption).foregroundColor((i.status == PodStatus.Succeeded.rawValue ? 0 : i.error) > 0 ? .red : .none) 36 | Text("not ready: \(i.status == PodStatus.Succeeded.rawValue ? 0 :i.notReady)").font(.caption).foregroundColor((i.status == PodStatus.Succeeded.rawValue ? 0 : i.notReady) > 0 ? .yellow : .none) 37 | } 38 | 39 | } 40 | } 41 | } 42 | 43 | } 44 | } 45 | .task { 46 | pods = await viewModel.model.podsByJob(in: .namespace(viewModel.ns), job: job.k8sName) 47 | } 48 | Section(header: "Labels and Annotations") { 49 | NavigationLink { 50 | List { 51 | ForEach((job.labels ?? [:]).sorted(by: >), id: \.key) { 52 | key, value in 53 | VStack(alignment: .leading) { 54 | Text(key) 55 | 56 | CaptionText(text: value) 57 | } 58 | } 59 | } 60 | 61 | } label: { 62 | Text("Labels") 63 | } 64 | NavigationLink { 65 | List { 66 | ForEach((job.annotations ?? [:]).sorted(by: >), id: \.key) { 67 | key, value in 68 | VStack(alignment: .leading) { 69 | Text(key) 70 | CaptionText(text: value) 71 | } 72 | } 73 | } 74 | } label: { 75 | Text("Annotations") 76 | } 77 | } 78 | 79 | Section(header: "Misc") { 80 | HStack{ 81 | Text("Namespace") 82 | Spacer() 83 | Text(job.namespace) 84 | } 85 | 86 | } 87 | } 88 | .navigationTitle("Job") 89 | } 90 | } 91 | 92 | struct JobView_Previews: PreviewProvider { 93 | static var previews: some View { 94 | JobView(job: Job(id: "123", name: "123", k8sName: [:], labels: ["l1":"l1v"], annotations: ["l1":"l1v"], namespace: "default", status: false), viewModel: ViewModel(viewContext: PersistenceController.preview.container.viewContext)) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /K8sCat/StorageView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StorageView.swift 3 | // K8sCat 4 | // 5 | // Created by 顾艳华 on 2022/12/29. 6 | // 7 | 8 | import SwiftUI 9 | import SwiftUIX 10 | 11 | struct StorageView: View { 12 | @FetchRequest( 13 | sortDescriptors: [], 14 | animation: .default) 15 | private var cluters: FetchedResults 16 | @State var search = "" 17 | @State var tabIndex = 0 18 | @ObservedObject var viewModel: ViewModel 19 | @State var showCluster = false 20 | @Environment(\.managedObjectContext) private var viewContext 21 | var body: some View { 22 | VStack { 23 | NavigationStack { 24 | HStack{ 25 | SearchBar(text: $search).padding(.horizontal) 26 | Button{showCluster = true}label: { 27 | Image(systemName: cluters.filter{$0.selected}.first?.icon ?? "0.circle") 28 | }.padding(.trailing) 29 | } 30 | StorageTabBar(tabIndex: $tabIndex).padding(.horizontal, 26) 31 | switch tabIndex { 32 | case 0: 33 | List { 34 | ForEach(viewModel.pv.filter{$0.name.contains(search.lowercased()) || search == ""}) { 35 | i in 36 | NavigationLink { 37 | PVView(pv: i, viewModel: viewModel) 38 | } label: { 39 | Image(systemName: "capsule.portrait") 40 | Text(i.name) 41 | } 42 | 43 | } 44 | }.listStyle(PlainListStyle()) 45 | .onAppear { 46 | Task { 47 | await viewModel.pv() 48 | } 49 | } 50 | .refreshable { 51 | viewModel.model.pvs = nil 52 | Task { 53 | await viewModel.pv() 54 | } 55 | } 56 | 57 | case 1: 58 | List { 59 | ForEach(viewModel.pvc.filter{$0.name.contains(search.lowercased()) || search == ""}) { 60 | i in 61 | NavigationLink { 62 | PVCView(pvc: i, viewModel: viewModel) 63 | } label: { 64 | Image(systemName: "shield") 65 | Text(i.name) 66 | } 67 | 68 | } 69 | }.listStyle(PlainListStyle()) 70 | .onAppear { 71 | Task { 72 | await viewModel.pvc() 73 | } 74 | } 75 | .refreshable { 76 | viewModel.model.pvcs = nil 77 | Task { 78 | await viewModel.pvc() 79 | } 80 | } 81 | 82 | default: 83 | EmptyView() 84 | } 85 | } 86 | }.sheet(isPresented: $showCluster){ 87 | ClusterView(viewModel: viewModel){ 88 | showCluster = false 89 | } 90 | .environment(\.managedObjectContext, viewContext) 91 | } 92 | } 93 | } 94 | 95 | struct StorageView_Previews: PreviewProvider { 96 | static var previews: some View { 97 | StorageView(viewModel: ViewModel(viewContext: PersistenceController.preview.container.viewContext)) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /K8sCat/inner/ReplicaView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReplicaView.swift 3 | // K8sCat 4 | // 5 | // Created by 顾艳华 on 2022/12/26. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ReplicaView: View { 11 | let replica: Replica 12 | let viewModel: ViewModel 13 | 14 | @State var pods: [Pod] = [] 15 | 16 | var body: some View { 17 | Form { 18 | Section(header: "Name") { 19 | Text(replica.name) 20 | } 21 | Section(header: "Status") { 22 | Text(replica.status ? "Ready" : "Pending") 23 | } 24 | Section(header: "Pods") { 25 | List { 26 | ForEach(pods) { 27 | i in 28 | NavigationLink { 29 | PodView(pod: i, viewModel: viewModel) 30 | } label: { 31 | Image(systemName: "tray.2") 32 | VStack(alignment: .leading) { 33 | Text(i.name) 34 | HStack{ 35 | Text("containers -").font(.caption) 36 | CaptionText(text: "expect: \(i.expect), ") 37 | Text("error: \(i.status == PodStatus.Succeeded.rawValue ? 0 : i.error)").font(.caption).foregroundColor((i.status == PodStatus.Succeeded.rawValue ? 0 : i.error) > 0 ? .red : .none) 38 | Text("not ready: \(i.status == PodStatus.Succeeded.rawValue ? 0 :i.notReady)").font(.caption).foregroundColor((i.status == PodStatus.Succeeded.rawValue ? 0 : i.notReady) > 0 ? .yellow : .none) 39 | } 40 | 41 | } 42 | } 43 | } 44 | 45 | } 46 | } 47 | .task { 48 | pods = await viewModel.model.podsByReplica(in: .namespace(viewModel.ns), replica: replica.k8sName) 49 | } 50 | Section(header: "Labels and Annotations") { 51 | NavigationLink { 52 | List { 53 | ForEach((replica.labels ?? [:]).sorted(by: >), id: \.key) { 54 | key, value in 55 | VStack(alignment: .leading) { 56 | Text(key) 57 | 58 | CaptionText(text: value) 59 | } 60 | } 61 | } 62 | 63 | } label: { 64 | Text("Labels") 65 | } 66 | NavigationLink { 67 | List { 68 | ForEach((replica.annotations ?? [:]).sorted(by: >), id: \.key) { 69 | key, value in 70 | VStack(alignment: .leading) { 71 | Text(key) 72 | CaptionText(text: value) 73 | } 74 | } 75 | } 76 | } label: { 77 | Text("Annotations") 78 | } 79 | } 80 | 81 | Section(header: "Misc") { 82 | HStack{ 83 | Text("Namespace") 84 | Spacer() 85 | Text(replica.namespace) 86 | } 87 | 88 | } 89 | } 90 | .navigationTitle("Replica Set") 91 | } 92 | } 93 | 94 | struct ReplicaView_Previews: PreviewProvider { 95 | static var previews: some View { 96 | ReplicaView(replica: Replica(id: "abc", name: "abc", k8sName: [:], labels: ["l1":"l1v"],annotations: ["a1":"a1v"],namespace: "default", status: true), viewModel: ViewModel(viewContext: PersistenceController.preview.container.viewContext)) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /K8sCat.xcodeproj/xcshareddata/xcschemes/K8sCat.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 34 | 40 | 41 | 42 | 45 | 51 | 52 | 53 | 54 | 55 | 65 | 67 | 73 | 74 | 75 | 76 | 82 | 84 | 90 | 91 | 92 | 93 | 95 | 96 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /K8sCat/yaml/static/js/787.eb3a1d75.chunk.js: -------------------------------------------------------------------------------- 1 | "use strict";(self.webpackChunkedit_yaml=self.webpackChunkedit_yaml||[]).push([[787],{787:function(e,t,n){n.r(t),n.d(t,{getCLS:function(){return y},getFCP:function(){return g},getFID:function(){return C},getLCP:function(){return P},getTTFB:function(){return D}});var i,r,a,o,u=function(e,t){return{name:e,value:void 0===t?-1:t,delta:0,entries:[],id:"v2-".concat(Date.now(),"-").concat(Math.floor(8999999999999*Math.random())+1e12)}},c=function(e,t){try{if(PerformanceObserver.supportedEntryTypes.includes(e)){if("first-input"===e&&!("PerformanceEventTiming"in self))return;var n=new PerformanceObserver((function(e){return e.getEntries().map(t)}));return n.observe({type:e,buffered:!0}),n}}catch(e){}},f=function(e,t){var n=function n(i){"pagehide"!==i.type&&"hidden"!==document.visibilityState||(e(i),t&&(removeEventListener("visibilitychange",n,!0),removeEventListener("pagehide",n,!0)))};addEventListener("visibilitychange",n,!0),addEventListener("pagehide",n,!0)},s=function(e){addEventListener("pageshow",(function(t){t.persisted&&e(t)}),!0)},m=function(e,t,n){var i;return function(r){t.value>=0&&(r||n)&&(t.delta=t.value-(i||0),(t.delta||void 0===i)&&(i=t.value,e(t)))}},v=-1,d=function(){return"hidden"===document.visibilityState?0:1/0},p=function(){f((function(e){var t=e.timeStamp;v=t}),!0)},l=function(){return v<0&&(v=d(),p(),s((function(){setTimeout((function(){v=d(),p()}),0)}))),{get firstHiddenTime(){return v}}},g=function(e,t){var n,i=l(),r=u("FCP"),a=function(e){"first-contentful-paint"===e.name&&(f&&f.disconnect(),e.startTime-1&&e(t)},r=u("CLS",0),a=0,o=[],v=function(e){if(!e.hadRecentInput){var t=o[0],i=o[o.length-1];a&&e.startTime-i.startTime<1e3&&e.startTime-t.startTime<5e3?(a+=e.value,o.push(e)):(a=e.value,o=[e]),a>r.value&&(r.value=a,r.entries=o,n())}},d=c("layout-shift",v);d&&(n=m(i,r,t),f((function(){d.takeRecords().map(v),n(!0)})),s((function(){a=0,T=-1,r=u("CLS",0),n=m(i,r,t)})))},E={passive:!0,capture:!0},w=new Date,L=function(e,t){i||(i=t,r=e,a=new Date,F(removeEventListener),S())},S=function(){if(r>=0&&r1e12?new Date:performance.now())-e.timeStamp;"pointerdown"==e.type?function(e,t){var n=function(){L(e,t),r()},i=function(){r()},r=function(){removeEventListener("pointerup",n,E),removeEventListener("pointercancel",i,E)};addEventListener("pointerup",n,E),addEventListener("pointercancel",i,E)}(t,e):L(t,e)}},F=function(e){["mousedown","keydown","touchstart","pointerdown"].forEach((function(t){return e(t,b,E)}))},C=function(e,t){var n,a=l(),v=u("FID"),d=function(e){e.startTimeperformance.now())return;n.entries=[t],e(n)}catch(e){}},"complete"===document.readyState?setTimeout(t,0):addEventListener("load",(function(){return setTimeout(t,0)}))}}}]); 2 | //# sourceMappingURL=787.eb3a1d75.chunk.js.map -------------------------------------------------------------------------------- /K8sCat/cluster/ConfigView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConfigView.swift 3 | // K8sCat 4 | // 5 | // Created by 顾艳华 on 2022/12/31. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ConfigView: View { 11 | @Environment(\.managedObjectContext) private var viewContext 12 | let first: Bool 13 | let close: () -> Void 14 | @State var name = "" 15 | @State var icon = "a.circle" 16 | @State var importMsg: String = "Import Kube Config" 17 | @State var content = "" 18 | @State var showErrorMessage = false 19 | @State var errorMessage = "" 20 | 21 | @State private var showingExporter = false 22 | 23 | var body: some View { 24 | NavigationStack { 25 | Form { 26 | 27 | Section(header: "Name"){ 28 | TextField(text: $name) 29 | .disableAutocorrection(true) 30 | .textInputAutocapitalization(.never) 31 | } 32 | Section(header: "Icon"){ 33 | Picker("icon", selection: $icon){ 34 | ForEach(["a.circle", "b.circle", "c.circle", "e.circle", "f.circle", "g.circle", "h.circle", "i.circle", "j.circle", "k.circle", "l.circle", "m.circle", "n.circle", "o.circle", "p.circle", "q.circle", "r.circle", "s.circle", "t.circle", "u.circle", "v.circle", "w.circle", "x.circle", "y.circle"], id: \.self) { 35 | Image(systemName: $0) 36 | } 37 | 38 | } 39 | 40 | } 41 | Section(header: "Kube Config"){ 42 | Text(importMsg).onTapGesture { 43 | showingExporter = true 44 | } 45 | } 46 | Button{ 47 | // save to core data 48 | addItem() 49 | 50 | } label: { 51 | Text("Save") 52 | } 53 | } 54 | 55 | } 56 | .alert(errorMessage, isPresented: $showErrorMessage){ 57 | Button("OK", role: .cancel) { 58 | showErrorMessage = false 59 | } 60 | } 61 | .fileImporter(isPresented: $showingExporter, allowedContentTypes: [.yaml]) { result in 62 | switch result { 63 | case .success(let url): 64 | let _ = url.startAccessingSecurityScopedResource() 65 | if let content = try? String(contentsOf: url) { 66 | self.content = content 67 | importMsg = "Imported" 68 | print("Import content is \(content)") 69 | } 70 | url.stopAccessingSecurityScopedResource() 71 | 72 | case .failure(let error): 73 | print(error.localizedDescription) 74 | } 75 | } 76 | } 77 | 78 | private func addItem() { 79 | if name.isEmpty { 80 | showErrorMessage = true 81 | errorMessage = "Input cluster name, please." 82 | return 83 | } 84 | if content.isEmpty { 85 | showErrorMessage = true 86 | errorMessage = "Import kube config, please." 87 | return 88 | } 89 | withAnimation { 90 | let newItem = ClusterEntry(context: viewContext) 91 | newItem.name = self.name 92 | newItem.type = ClusterType.KubeConfig.rawValue 93 | newItem.icon = icon 94 | newItem.config = self.content 95 | if first { 96 | newItem.selected = true 97 | } 98 | 99 | do { 100 | try viewContext.save() 101 | } catch { 102 | // Replace this implementation with code to handle the error appropriately. 103 | // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. 104 | let nsError = error as NSError 105 | fatalError("Unresolved error \(nsError), \(nsError.userInfo)") 106 | } 107 | } 108 | close() 109 | } 110 | } 111 | 112 | struct ConfigView_Previews: PreviewProvider { 113 | static var previews: some View { 114 | ConfigView(first: true){} 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /K8sCat/inner/NodeView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NodeView.swift 3 | // K8sCat 4 | // 5 | // Created by 顾艳华 on 2022/12/27. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct NodeView: View { 11 | let node: Node 12 | let viewModel: ViewModel 13 | var body: some View { 14 | Form { 15 | Section(header: "Name") { 16 | Text(node.name) 17 | } 18 | // Section(header: "Status") { 19 | // Text(node.status) 20 | // } 21 | Section(header: "Role") { 22 | HStack{ 23 | Text("Etcd") 24 | Spacer() 25 | Text(String(node.etcd)) 26 | } 27 | HStack{ 28 | Text("Control Plane") 29 | Spacer() 30 | Text(String(node.controlPlane)) 31 | } 32 | HStack{ 33 | Text("Worker") 34 | Spacer() 35 | Text(String(node.worker)) 36 | } 37 | HStack{ 38 | Text("Agent") 39 | Spacer() 40 | Text(String(node.agent)) 41 | } 42 | } 43 | // Section(header: "Pods") { 44 | // List { 45 | // ForEach(viewModel.model.podsByDeployment(in: .namespace(viewModel.ns), deployment: deployment.k8sName)) { 46 | // i in 47 | // NavigationLink { 48 | // PodView(pod: i, viewModel: viewModel) 49 | // } label: { 50 | // VStack(alignment: .leading) { 51 | // Text(i.name).foregroundColor(.green) 52 | // HStack{ 53 | // CaptionText(text: "expect: \(i.expect), ") 54 | // CaptionText(text: "pendding: \(i.pending)").foregroundColor(i.pending > 0 ? .red : .none) 55 | // } 56 | // 57 | // } 58 | // } 59 | // } 60 | // } 61 | // } 62 | Section(header: "Labels and Annotations") { 63 | NavigationLink { 64 | List { 65 | ForEach((node.labels ?? [:]).sorted(by: >), id: \.key) { 66 | key, value in 67 | VStack(alignment: .leading) { 68 | Text(key) 69 | 70 | CaptionText(text: value) 71 | } 72 | } 73 | } 74 | 75 | } label: { 76 | Text("Labels") 77 | } 78 | NavigationLink { 79 | List { 80 | ForEach((node.annotations ?? [:]).sorted(by: >), id: \.key) { 81 | key, value in 82 | VStack(alignment: .leading) { 83 | Text(key) 84 | CaptionText(text: value) 85 | } 86 | } 87 | } 88 | } label: { 89 | Text("Annotations") 90 | } 91 | } 92 | 93 | Section(header: "Misc") { 94 | HStack{ 95 | Text("Host Name") 96 | Spacer() 97 | Text(node.hostName) 98 | } 99 | HStack{ 100 | Text("Kube Version") 101 | Spacer() 102 | Text(node.version) 103 | } 104 | HStack{ 105 | Text("Arch") 106 | Spacer() 107 | Text(node.arch) 108 | } 109 | HStack{ 110 | Text("OS") 111 | Spacer() 112 | Text(node.os) 113 | } 114 | } 115 | } 116 | .navigationTitle("Node") 117 | } 118 | } 119 | 120 | //struct NodeView_Previews: PreviewProvider { 121 | // static var previews: some View { 122 | // NodeView(node: Node(id: "123", name: "123", hostName: "10.0.0.4", arch: "x86", os: "linux", labels: ["l1":"l1v"],annotations: ["a1":"a1v"], etcd: true,worker: true,controlPlane: true, version: "1.1"),viewModel: ViewModel(viewContext: PersistenceController.preview.container.viewContext)) 123 | // } 124 | //} 125 | -------------------------------------------------------------------------------- /K8sCat/id/Identifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Identifier.swift 3 | // K8sCat 4 | // 5 | // Created by 顾艳华 on 2022/12/21. 6 | // 7 | 8 | import Foundation 9 | import SwiftkubeClient 10 | import NIO 11 | import Yams 12 | import NIOSSL 13 | import AsyncHTTPClient 14 | 15 | 16 | protocol CertIdentifier { 17 | func config() throws -> KubernetesClientConfig? 18 | } 19 | 20 | struct AWS: CertIdentifier { 21 | let awsId: String 22 | let awsSecret: String 23 | let region: String 24 | let clusterName: String 25 | 26 | func config() throws -> KubernetesClientConfig? { 27 | let token = MyAWSClient(ak: awsId, sk: awsSecret, region: region, clusterName: clusterName).getToken() 28 | let c = MyAWSClient(ak: awsId, sk: awsSecret, region: region, clusterName: clusterName).getCluster() 29 | if let c = c,let token = token { 30 | if let data = Data(base64Encoded: c.ca) { 31 | let caCert = try NIOSSLCertificate.fromPEMBytes([UInt8](data)) 32 | let authentication = KubernetesClientAuthentication.bearer(token: token) 33 | if let url = URL(string: c.sever) { 34 | let config = KubernetesClientConfig( 35 | masterURL: url, 36 | namespace: "default", 37 | authentication: authentication, 38 | trustRoots: NIOSSLTrustRoots.certificates(caCert), 39 | insecureSkipTLSVerify: false, 40 | timeout: HTTPClient.Configuration.Timeout.init(connect: .seconds(1), read: .seconds(10)), 41 | redirectConfiguration: HTTPClient.Configuration.RedirectConfiguration.follow(max: 5, allowCycles: false) 42 | ) 43 | 44 | return config 45 | } else { 46 | return nil 47 | } 48 | } else { 49 | return nil 50 | } 51 | } else { 52 | return nil 53 | } 54 | 55 | } 56 | 57 | 58 | } 59 | 60 | struct Config: CertIdentifier { 61 | let content: String 62 | 63 | func config() throws -> KubernetesClientConfig? { 64 | let decoder = YAMLDecoder() 65 | guard let kubeConfig = try? decoder.decode(KubeConfig.self, from: content) else { 66 | return nil 67 | } 68 | if kubeConfig.clusters == nil || kubeConfig.contexts == nil || kubeConfig.users == nil { 69 | return nil 70 | } 71 | // print("\(kubeConfig)") 72 | let currentContext = kubeConfig.currentContext 73 | let ctxs = kubeConfig.contexts?.filter{$0.name == currentContext} 74 | if ctxs == nil || ctxs!.isEmpty { 75 | return nil 76 | } 77 | let context = ctxs!.first! 78 | let cluters = kubeConfig.clusters?.filter{$0.name == context.context.cluster} 79 | if cluters == nil || cluters!.isEmpty { 80 | return nil 81 | } 82 | let cluster = cluters!.first! 83 | let users = kubeConfig.users?.filter{$0.name == context.context.user} 84 | if users == nil || users!.isEmpty { 85 | return nil 86 | } 87 | let user = users!.first! 88 | if let clientCertificateData = user.authInfo.clientCertificateData,let clientKeyData = user.authInfo.clientKeyData,let certificateAuthorityData = cluster.cluster.certificateAuthorityData { 89 | let p = try NIOSSLCertificate(bytes: .init(clientCertificateData), format: NIOSSLSerializationFormats.pem) 90 | let k = try NIOSSLPrivateKey(bytes: .init(clientKeyData), format: NIOSSLSerializationFormats.pem) 91 | 92 | let authentication = KubernetesClientAuthentication.x509(clientCertificate: p, clientKey: k) 93 | 94 | let caCert = try NIOSSLCertificate.fromPEMBytes([UInt8](certificateAuthorityData)) 95 | if let url = URL(string: cluster.cluster.server) { 96 | let config = KubernetesClientConfig( 97 | masterURL: url, 98 | namespace: context.context.namespace ?? "default", 99 | authentication: authentication, 100 | trustRoots: NIOSSLTrustRoots.certificates(caCert), 101 | insecureSkipTLSVerify: false, 102 | timeout: HTTPClient.Configuration.Timeout.init(connect: .seconds(1), read: .seconds(10)), 103 | redirectConfiguration: HTTPClient.Configuration.RedirectConfiguration.follow(max: 5, allowCycles: false) 104 | ) 105 | return config 106 | } else { 107 | return nil 108 | } 109 | } else { 110 | return nil 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /K8sCat/inner/CronJobView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CronJobView.swift 3 | // K8sCat 4 | // 5 | // Created by 顾艳华 on 2022/12/26. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct CronJobView: View { 11 | let cronJob: CronJob 12 | let viewModel: ViewModel 13 | 14 | @State var showYaml = false 15 | var body: some View { 16 | Form { 17 | Section(header: "Name") { 18 | Text(cronJob.name) 19 | } 20 | Section(header: "Schedule") { 21 | Text(cronJob.schedule) 22 | } 23 | // link job 24 | // Section(header: "Pods") { 25 | // List { 26 | // ForEach(viewModel.model.podsByCronJob(in: .namespace(viewModel.ns), cronJob: cronJob.k8sName)) { 27 | // i in 28 | // NavigationLink { 29 | // PodView(pod: i, viewModel: viewModel) 30 | // } label: { 31 | // Image(systemName: "tray.2") 32 | // VStack(alignment: .leading) { 33 | // Text(i.name).foregroundColor(.green) 34 | // HStack{ 35 | // Text("containers -").font(.caption) 36 | // CaptionText(text: "expect: \(i.expect), ") 37 | // CaptionText(text: "warning: \(i.warning)").foregroundColor(i.warning > 0 ? .red : .none) 38 | // } 39 | // 40 | // } 41 | // } 42 | // } 43 | // } 44 | // } 45 | Section(header: "Labels and Annotations") { 46 | NavigationLink { 47 | List { 48 | ForEach((cronJob.labels ?? [:]).sorted(by: >), id: \.key) { 49 | key, value in 50 | VStack(alignment: .leading) { 51 | Text(key) 52 | 53 | CaptionText(text: value) 54 | } 55 | } 56 | } 57 | 58 | } label: { 59 | Text("Labels") 60 | } 61 | NavigationLink { 62 | List { 63 | ForEach((cronJob.annotations ?? [:]).sorted(by: >), id: \.key) { 64 | key, value in 65 | VStack(alignment: .leading) { 66 | Text(key) 67 | CaptionText(text: value) 68 | } 69 | } 70 | } 71 | } label: { 72 | Text("Annotations") 73 | } 74 | } 75 | 76 | Section(header: "Misc") { 77 | HStack{ 78 | Text("Namespace") 79 | Spacer() 80 | Text(cronJob.namespace) 81 | } 82 | 83 | } 84 | }.toolbar{ 85 | Menu { 86 | Button { 87 | // do something 88 | let yaml = cronJob.encodeYaml(client: viewModel.model.client) 89 | print("Yaml: \(yaml)") 90 | showYaml = true 91 | // deployment.decodeYaml(client: viewModel.model.client, yaml: yaml) 92 | } label: { 93 | Text("View/Edit Yaml") 94 | Image(systemName: "note.text") 95 | } 96 | // Button { 97 | // // do something 98 | // } label: { 99 | // Text("Delete Resource") 100 | // Image(systemName: "trash") 101 | // } 102 | } label: { 103 | Image(systemName: "ellipsis") 104 | } 105 | }.sheet(isPresented: $showYaml){ 106 | #if os(iOS) 107 | YamlWebView(yamlble: cronJob, model: viewModel.model) { 108 | showYaml = false 109 | } 110 | #endif 111 | Button{ 112 | Task { 113 | await urlScheme(yamlble: cronJob, client: viewModel.model.client) 114 | } 115 | }label: { 116 | Text("Load yaml via Yamler") 117 | }.padding() 118 | } 119 | .navigationTitle("Cron Job") 120 | } 121 | } 122 | 123 | //struct CronJobView_Previews: PreviewProvider { 124 | // static var previews: some View { 125 | // CronJobView(cronJob: CronJob(id: "123", name: "123", k8sName: "123", labels: ["l1":"l1v"], annotations: ["l1":"l1v"], namespace: "default", schedule: "10 / 5 * * *"), viewModel: ViewModel(viewContext: PersistenceController.preview.container.viewContext)) 126 | // } 127 | //} 128 | -------------------------------------------------------------------------------- /K8sCat/inner/ServiceView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ServiceView.swift 3 | // K8sCat 4 | // 5 | // Created by 顾艳华 on 2022/12/26. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ServiceView: View { 11 | let service: Service 12 | let viewModel: ViewModel 13 | 14 | @State var pods: [Pod] = [] 15 | var body: some View { 16 | Form { 17 | Section(header: "Name") { 18 | Text(service.name) 19 | } 20 | Section(header: "Pods") { 21 | List { 22 | ForEach(pods) { 23 | i in 24 | NavigationLink { 25 | PodView(pod: i, viewModel: viewModel) 26 | } label: { 27 | Image(systemName: "tray.2") 28 | VStack(alignment: .leading) { 29 | Text(i.name).foregroundColor(i.status == PodStatus.Failed.rawValue ? .red : (i.status == PodStatus.Running.rawValue || i.status == PodStatus.Succeeded.rawValue ? .green : .yellow)) 30 | HStack{ 31 | Text("containers -").font(.caption) 32 | CaptionText(text: "expect: \(i.expect), ") 33 | Text("error: \(i.status == PodStatus.Succeeded.rawValue ? 0 : i.error)").font(.caption).foregroundColor((i.status == PodStatus.Succeeded.rawValue ? 0 : i.error) > 0 ? .red : .none) 34 | Text("not ready: \(i.status == PodStatus.Succeeded.rawValue ? 0 :i.notReady)").font(.caption).foregroundColor((i.status == PodStatus.Succeeded.rawValue ? 0 : i.notReady) > 0 ? .yellow : .none) 35 | } 36 | 37 | } 38 | } 39 | } 40 | } 41 | 42 | } 43 | .task { 44 | pods = await viewModel.model.podsByService(in: .namespace(viewModel.ns), service: service.k8sName) 45 | } 46 | Section(header: "Labels and Annotations") { 47 | NavigationLink { 48 | List { 49 | ForEach((service.labels ?? [:]).sorted(by: >), id: \.key) { 50 | key, value in 51 | VStack(alignment: .leading) { 52 | Text(key) 53 | 54 | CaptionText(text: value) 55 | } 56 | } 57 | } 58 | 59 | } label: { 60 | Text("Labels") 61 | } 62 | NavigationLink { 63 | List { 64 | ForEach((service.annotations ?? [:]).sorted(by: >), id: \.key) { 65 | key, value in 66 | VStack(alignment: .leading) { 67 | Text(key) 68 | CaptionText(text: value) 69 | } 70 | } 71 | } 72 | } label: { 73 | Text("Annotations") 74 | } 75 | } 76 | Section(header: "Ip") { 77 | List { 78 | 79 | VStack(alignment: .leading) { 80 | Text("Cluster IPs") 81 | ForEach(service.clusterIps ?? ["None"], id: \.self) { 82 | ip in 83 | 84 | CaptionText(text: ip) 85 | 86 | } 87 | } 88 | 89 | VStack(alignment: .leading) { 90 | Text("External IPs") 91 | ForEach(service.externalIps ?? ["None"], id: \.self) { 92 | ip in 93 | 94 | CaptionText(text: ip) 95 | 96 | } 97 | } 98 | } 99 | } 100 | Section(header: "Misc") { 101 | HStack{ 102 | Text("Namespace") 103 | Spacer() 104 | Text(service.namespace) 105 | } 106 | 107 | } 108 | } 109 | .navigationTitle("Service") 110 | } 111 | } 112 | 113 | struct ServiceView_Previews: PreviewProvider { 114 | static var previews: some View { 115 | ServiceView(service: Service(id: "123", name: "123", k8sName: [:], type: "clusterIP", clusterIps: ["10.0.0.3"], externalIps: nil, labels: ["l1":"l1v"],annotations: ["a1":"a1v"],namespace: "default"), viewModel: ViewModel(viewContext: PersistenceController.preview.container.viewContext)) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /K8sCat/inner/DaemonView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DaemonView.swift 3 | // K8sCat 4 | // 5 | // Created by 顾艳华 on 2022/12/26. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct DaemonView: View { 11 | let daemon: Daemon 12 | let viewModel: ViewModel 13 | 14 | @State var pods: [Pod] = [] 15 | @State var showYaml = false 16 | var body: some View { 17 | Form { 18 | Section(header: "Name") { 19 | Text(daemon.name) 20 | } 21 | Section(header: "Status") { 22 | Text(daemon.status ? "Ready" : "Schedule") 23 | } 24 | Section(header: "Pods") { 25 | List { 26 | ForEach(pods) { 27 | i in 28 | NavigationLink { 29 | PodView(pod: i, viewModel: viewModel) 30 | } label: { 31 | Image(systemName: "tray.2") 32 | VStack(alignment: .leading) { 33 | Text(i.name) 34 | HStack{ 35 | Text("containers -").font(.caption) 36 | CaptionText(text: "expect: \(i.expect), ") 37 | Text("error: \(i.status == PodStatus.Succeeded.rawValue ? 0 : i.error)").font(.caption).foregroundColor((i.status == PodStatus.Succeeded.rawValue ? 0 : i.error) > 0 ? .red : .none) 38 | Text("not ready: \(i.status == PodStatus.Succeeded.rawValue ? 0 :i.notReady)").font(.caption).foregroundColor((i.status == PodStatus.Succeeded.rawValue ? 0 : i.notReady) > 0 ? .yellow : .none) 39 | } 40 | 41 | } 42 | } 43 | } 44 | 45 | } 46 | } 47 | .task { 48 | pods = await viewModel.model.podsByDaemon(in: .namespace(viewModel.ns), daemon: daemon.k8sName) 49 | } 50 | Section(header: "Labels and Annotations") { 51 | NavigationLink { 52 | List { 53 | ForEach((daemon.labels ?? [:]).sorted(by: >), id: \.key) { 54 | key, value in 55 | VStack(alignment: .leading) { 56 | Text(key) 57 | 58 | CaptionText(text: value) 59 | } 60 | } 61 | } 62 | 63 | } label: { 64 | Text("Labels") 65 | } 66 | NavigationLink { 67 | List { 68 | ForEach((daemon.annotations ?? [:]).sorted(by: >), id: \.key) { 69 | key, value in 70 | VStack(alignment: .leading) { 71 | Text(key) 72 | CaptionText(text: value) 73 | } 74 | } 75 | } 76 | } label: { 77 | Text("Annotations") 78 | } 79 | } 80 | 81 | Section(header: "Misc") { 82 | HStack{ 83 | Text("Namespace") 84 | Spacer() 85 | Text(daemon.namespace) 86 | } 87 | 88 | } 89 | }.toolbar{ 90 | Menu { 91 | Button { 92 | // do something 93 | // let yaml = daemon.encodeYaml(client: viewModel.model.client) 94 | // print("Yaml: \(yaml)") 95 | showYaml = true 96 | // deployment.decodeYaml(client: viewModel.model.client, yaml: yaml) 97 | } label: { 98 | Text("View/Edit Yaml") 99 | Image(systemName: "note.text") 100 | } 101 | // Button { 102 | // // do something 103 | // } label: { 104 | // Text("Delete Resource") 105 | // Image(systemName: "trash") 106 | // } 107 | } label: { 108 | Image(systemName: "ellipsis") 109 | } 110 | }.sheet(isPresented: $showYaml){ 111 | #if os(iOS) 112 | YamlWebView(yamlble: daemon, model: viewModel.model) { 113 | showYaml = false 114 | } 115 | #endif 116 | Button{ 117 | Task { 118 | await urlScheme(yamlble: daemon, client: viewModel.model.client) 119 | } 120 | }label: { 121 | Text("Load yaml via Yamler") 122 | }.padding() 123 | } 124 | .navigationTitle("Daemon Set") 125 | } 126 | } 127 | 128 | struct DaemonView_Previews: PreviewProvider { 129 | static var previews: some View { 130 | DaemonView(daemon: Daemon(id: "123", name: "123", k8sName: [:], labels: ["l1":"l1v"],annotations: ["a1":"a1v"],namespace: "default", status: true, raw: nil), viewModel: ViewModel(viewContext: PersistenceController.preview.container.viewContext)) 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /K8sCat/inner/StatefulView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StatefulView.swift 3 | // K8sCat 4 | // 5 | // Created by 顾艳华 on 2022/12/26. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct StatefulView: View { 11 | let stateful: Stateful 12 | let viewModel: ViewModel 13 | 14 | @State var pods: [Pod] = [] 15 | 16 | @State var showYaml = false 17 | var body: some View { 18 | Form { 19 | Section(header: "Name") { 20 | Text(stateful.name) 21 | } 22 | Section(header: "Status") { 23 | Text(stateful.status ? "Ready" : "Pending") 24 | } 25 | Section(header: "Pods") { 26 | List { 27 | ForEach(pods) { 28 | i in 29 | NavigationLink { 30 | PodView(pod: i, viewModel: viewModel) 31 | } label: { 32 | Image(systemName: "tray.2") 33 | VStack(alignment: .leading) { 34 | Text(i.name) 35 | HStack{ 36 | Text("containers -").font(.caption) 37 | CaptionText(text: "expect: \(i.expect), ") 38 | Text("error: \(i.status == PodStatus.Succeeded.rawValue ? 0 : i.error)").font(.caption).foregroundColor((i.status == PodStatus.Succeeded.rawValue ? 0 : i.error) > 0 ? .red : .none) 39 | Text("not ready: \(i.status == PodStatus.Succeeded.rawValue ? 0 :i.notReady)").font(.caption).foregroundColor((i.status == PodStatus.Succeeded.rawValue ? 0 : i.notReady) > 0 ? .yellow : .none) 40 | } 41 | 42 | } 43 | } 44 | } 45 | } 46 | } 47 | .task { 48 | pods = await viewModel.model.podsByStateful(in: .namespace(viewModel.ns), stateful: stateful.k8sName) 49 | } 50 | Section(header: "Labels and Annotations") { 51 | NavigationLink { 52 | List { 53 | ForEach((stateful.labels ?? [:]).sorted(by: >), id: \.key) { 54 | key, value in 55 | VStack(alignment: .leading) { 56 | Text(key) 57 | 58 | CaptionText(text: value) 59 | } 60 | } 61 | } 62 | 63 | } label: { 64 | Text("Labels") 65 | } 66 | NavigationLink { 67 | List { 68 | ForEach((stateful.annotations ?? [:]).sorted(by: >), id: \.key) { 69 | key, value in 70 | VStack(alignment: .leading) { 71 | Text(key) 72 | CaptionText(text: value) 73 | } 74 | } 75 | } 76 | } label: { 77 | Text("Annotations") 78 | } 79 | } 80 | 81 | Section(header: "Misc") { 82 | HStack{ 83 | Text("Namespace") 84 | Spacer() 85 | Text(stateful.namespace) 86 | } 87 | 88 | } 89 | }.toolbar{ 90 | Menu { 91 | Button { 92 | // do something 93 | // let yaml = stateful.encodeYaml(client: viewModel.model.client) 94 | // print("Yaml: \(yaml)") 95 | showYaml = true 96 | // deployment.decodeYaml(client: viewModel.model.client, yaml: yaml) 97 | } label: { 98 | Text("View/Edit Yaml") 99 | Image(systemName: "note.text") 100 | } 101 | // Button { 102 | // // do something 103 | // } label: { 104 | // Text("Delete Resource") 105 | // Image(systemName: "trash") 106 | // } 107 | } label: { 108 | Image(systemName: "ellipsis") 109 | } 110 | }.sheet(isPresented: $showYaml){ 111 | #if os(iOS) 112 | YamlWebView(yamlble: stateful, model: viewModel.model) { 113 | showYaml = false 114 | } 115 | #endif 116 | Button{ 117 | Task { 118 | await urlScheme(yamlble: stateful, client: viewModel.model.client) 119 | } 120 | }label: { 121 | Text("Load yaml via Yamler") 122 | }.padding() 123 | } 124 | .navigationTitle("Stateful Set") 125 | } 126 | } 127 | 128 | struct StatefulView_Previews: PreviewProvider { 129 | static var previews: some View { 130 | StatefulView(stateful: Stateful(id: "123", name: "123", k8sName: [:], labels: ["l1":"l1v"],annotations: ["a1":"a1v"],namespace: "default", status: true, raw: nil), viewModel: ViewModel(viewContext: PersistenceController.preview.container.viewContext)) 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /K8sCat/tab/TabBarButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TabBarButton.swift 3 | // K8sCat 4 | // 5 | // Created by 顾艳华 on 2022/12/20. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct TabBarButton: View { 11 | let text: String 12 | @Binding var isSelected: Bool 13 | var body: some View { 14 | Text(text) 15 | .fontWeight(isSelected ? .heavy : .regular) 16 | .font(.custom("Avenir", size: 16)) 17 | .padding(.vertical, 10) 18 | .border(width: isSelected ? 2 : 1, edges: [.bottom], color: .systemGray) 19 | } 20 | } 21 | 22 | struct NamespacesTabBar: View { 23 | @Binding var tabIndex: Int 24 | var body: some View { 25 | ScrollView(.horizontal, showsIndicators: false) { 26 | HStack(spacing: 20) { 27 | Group { 28 | TabBarButton(text: "Pods", isSelected: .constant(tabIndex == 0)) 29 | .onTapGesture { onButtonTapped(index: 0) } 30 | TabBarButton(text: "Deployments", isSelected: .constant(tabIndex == 1)) 31 | .onTapGesture { onButtonTapped(index: 1) } 32 | TabBarButton(text: "Job", isSelected: .constant(tabIndex == 2)) 33 | .onTapGesture { onButtonTapped(index: 2) } 34 | TabBarButton(text: "Cron Job", isSelected: .constant(tabIndex == 3)) 35 | .onTapGesture { onButtonTapped(index: 3) } 36 | TabBarButton(text: "Statefull Sets", isSelected: .constant(tabIndex == 4)) 37 | .onTapGesture { onButtonTapped(index: 4) } 38 | TabBarButton(text: "Service", isSelected: .constant(tabIndex == 5)) 39 | .onTapGesture { onButtonTapped(index: 5) } 40 | } 41 | Group { 42 | TabBarButton(text: "Config Map", isSelected: .constant(tabIndex == 6)) 43 | .onTapGesture { onButtonTapped(index: 6) } 44 | TabBarButton(text: "Secrets", isSelected: .constant(tabIndex == 7)) 45 | .onTapGesture { onButtonTapped(index: 7) } 46 | TabBarButton(text: "Daemon Sets", isSelected: .constant(tabIndex == 8)) 47 | .onTapGesture { onButtonTapped(index: 8) } 48 | TabBarButton(text: "Replica Sets", isSelected: .constant(tabIndex == 9)) 49 | .onTapGesture { onButtonTapped(index: 9) } 50 | TabBarButton(text: "‎Horizontal Pod Autoscaler", isSelected: .constant(tabIndex == 10)) 51 | .onTapGesture { onButtonTapped(index: 10) } 52 | } 53 | 54 | } 55 | } 56 | .border(width: 1, edges: [.bottom], color: .systemGray) 57 | } 58 | 59 | private func onButtonTapped(index: Int) { 60 | withAnimation { tabIndex = index } 61 | } 62 | } 63 | 64 | struct NodesTabBar: View { 65 | @Binding var tabIndex: Int 66 | var body: some View { 67 | ScrollView(.horizontal, showsIndicators: false) { 68 | HStack(spacing: 20) { 69 | TabBarButton(text: "Nodes", isSelected: .constant(tabIndex == 0)) 70 | .onTapGesture { onButtonTapped(index: 0) } 71 | } 72 | } 73 | .border(width: 1, edges: [.bottom], color: .black) 74 | } 75 | 76 | private func onButtonTapped(index: Int) { 77 | withAnimation { tabIndex = index } 78 | } 79 | } 80 | 81 | struct StorageTabBar: View { 82 | @Binding var tabIndex: Int 83 | var body: some View { 84 | ScrollView(.horizontal, showsIndicators: false) { 85 | HStack(spacing: 20) { 86 | TabBarButton(text: "Persistent Volumes", isSelected: .constant(tabIndex == 0)) 87 | .onTapGesture { onButtonTapped(index: 0) } 88 | TabBarButton(text: "Persistent Volumes Claim", isSelected: .constant(tabIndex == 1)) 89 | .onTapGesture { onButtonTapped(index: 1) } 90 | } 91 | } 92 | .border(width: 1, edges: [.bottom], color: .black) 93 | } 94 | 95 | private func onButtonTapped(index: Int) { 96 | withAnimation { tabIndex = index } 97 | } 98 | } 99 | 100 | struct EdgeBorder: Shape { 101 | 102 | var width: CGFloat 103 | var edges: [Edge] 104 | 105 | func path(in rect: CGRect) -> Path { 106 | var path = Path() 107 | for edge in edges { 108 | var x: CGFloat { 109 | switch edge { 110 | case .top, .bottom, .leading: return rect.minX 111 | case .trailing: return rect.maxX - width 112 | } 113 | } 114 | 115 | var y: CGFloat { 116 | switch edge { 117 | case .top, .leading, .trailing: return rect.minY 118 | case .bottom: return rect.maxY - width 119 | } 120 | } 121 | 122 | var w: CGFloat { 123 | switch edge { 124 | case .top, .bottom: return rect.width 125 | case .leading, .trailing: return self.width 126 | } 127 | } 128 | 129 | var h: CGFloat { 130 | switch edge { 131 | case .top, .bottom: return self.width 132 | case .leading, .trailing: return rect.height 133 | } 134 | } 135 | path.addPath(Path(CGRect(x: x, y: y, width: w, height: h))) 136 | } 137 | return path 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /K8sCat/cluster/ClusterView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ClusterView.swift 3 | // K8sCat 4 | // 5 | // Created by 顾艳华 on 2022/12/30. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ClusterView: View { 11 | 12 | let viewModel: ViewModel 13 | let close: () -> Void 14 | @Environment(\.managedObjectContext) private var viewContext 15 | @FetchRequest( 16 | sortDescriptors: [], 17 | animation: .default) 18 | private var cluters: FetchedResults 19 | 20 | @State var showClusterType = false 21 | @State var selectedItem: String? 22 | var body: some View { 23 | HStack { 24 | Spacer() 25 | Button{ 26 | showClusterType = true 27 | }label:{ 28 | Image(systemName: "plus") 29 | }.padding(.trailing) 30 | #if os(iOS) 31 | EditButton() 32 | #endif 33 | } 34 | .sheet(isPresented: $showClusterType){ 35 | ClusterTypeView(first: cluters.isEmpty){ 36 | showClusterType = false 37 | } 38 | .environment(\.managedObjectContext, viewContext) 39 | } 40 | .padding() 41 | List(selection: $selectedItem) { 42 | ForEach(cluters.map{Cluster(id: $0.name ?? "", name: $0.name ?? "", icon: 43 | $0.icon ?? "", kubeConfig: $0.config, selected: $0.selected )}) { 44 | i in 45 | HStack { 46 | // Image(systemName: i.selected ? "circle.fill" : "circle") 47 | Image(systemName: i.selected ? i.icon + ".fill" : i.icon) 48 | Text(i.name) 49 | } 50 | }.onDelete{ 51 | sets in 52 | deleteItems(offsets: sets) 53 | }.onChange(of: selectedItem ?? ""){ 54 | c in 55 | selectItem(id: c) 56 | close() 57 | } 58 | } 59 | .listStyle(PlainListStyle()) 60 | 61 | 62 | } 63 | 64 | 65 | 66 | fileprivate func selectNext() { 67 | var allUnSelected = true 68 | for cluster in cluters { 69 | if cluster.selected { 70 | allUnSelected = false 71 | } 72 | } 73 | if !cluters.isEmpty && allUnSelected { 74 | if let id = cluters.first?.name { 75 | selectItem(id: id) 76 | } 77 | } 78 | } 79 | 80 | private func deleteItems(offsets: IndexSet) { 81 | withAnimation { 82 | let delete = offsets.map { cluters[$0] } 83 | delete.forEach{if $0.demo && $0.selected { 84 | viewModel.model.hasAndSelectDemo = false 85 | 86 | }} 87 | delete.forEach{ 88 | if $0.selected { 89 | viewModel.model.clearAll() 90 | } 91 | } 92 | delete.forEach(viewContext.delete) 93 | 94 | do { 95 | try viewContext.save() 96 | selectNext() 97 | } catch { 98 | // Replace this implementation with code to handle the error appropriately. 99 | // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. 100 | let nsError = error as NSError 101 | fatalError("Unresolved error \(nsError), \(nsError.userInfo)") 102 | } 103 | } 104 | } 105 | 106 | private func selectItem(id: String) { 107 | withAnimation { 108 | for cluster in cluters { 109 | if cluster.name == id { 110 | cluster.selected = true 111 | if !cluster.demo { 112 | viewModel.model.hasAndSelectDemo = false 113 | } 114 | 115 | } else { 116 | cluster.selected = false 117 | } 118 | } 119 | 120 | do { 121 | try viewContext.save() 122 | viewModel.select(viewContext: viewContext) 123 | let namespaces = viewModel.namespace 124 | viewModel.ns = namespaces.isEmpty ? "default" : namespaces.first! 125 | } catch { 126 | // Replace this implementation with code to handle the error appropriately. 127 | // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. 128 | let nsError = error as NSError 129 | fatalError("Unresolved error \(nsError), \(nsError.userInfo)") 130 | } 131 | } 132 | } 133 | } 134 | 135 | struct Cluster: Identifiable, Hashable { 136 | var id: String 137 | let name: String 138 | let icon: String 139 | let kubeConfig: String? 140 | let selected: Bool 141 | 142 | } 143 | 144 | enum ClusterType: String, CaseIterable { 145 | case KubeConfig 146 | case Demo 147 | case Aliyun 148 | case AWS 149 | case GCP 150 | case Azuse 151 | case DO 152 | } 153 | 154 | struct ClusterView_Previews: PreviewProvider { 155 | static var previews: some View { 156 | ClusterView(viewModel: ViewModel(viewContext: PersistenceController.preview.container.viewContext)){} 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /K8sCat/id/MyAWSClient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AWSClient.swift 3 | // K8sCat 4 | // 5 | // Created by 顾艳华 on 2023/1/6. 6 | // 7 | 8 | import Foundation 9 | import SotoEKS 10 | import SotoSTS 11 | 12 | struct MyAWSClient { 13 | 14 | 15 | let ak: String 16 | let sk: String 17 | let region: String 18 | let clusterName: String 19 | 20 | 21 | static let TOKEN_PREFIX = "k8s-aws-v1." 22 | func getCluster() -> AWSCluster? { 23 | let client = AWSClient( 24 | credentialProvider: .static(accessKeyId: ak, secretAccessKey: sk), 25 | httpClientProvider: .createNew 26 | ) 27 | var eks: EKS? 28 | switch region { 29 | case "us-west-1": eks = EKS(client: client, region: .uswest1) 30 | case "us-east-1": eks = EKS(client: client, region: .useast1) 31 | case "us-east-2": eks = EKS(client: client, region: .useast2) 32 | case "us-west-2": eks = EKS(client: client, region: .uswest2) 33 | case "ca-central-1": eks = EKS(client: client, region: .cacentral1) 34 | case "eu-west-1": eks = EKS(client: client, region: .euwest1) 35 | case "eu-west-2": eks = EKS(client: client, region: .euwest2) 36 | case "eu-west-3": eks = EKS(client: client, region: .euwest3) 37 | case "eu-south-1": eks = EKS(client: client, region: .eusouth1) 38 | case "eu-south-2": eks = EKS(client: client, region: .eusouth2) 39 | case "eu-central-1": eks = EKS(client: client, region: .eucentral1) 40 | case "eu-central-2": eks = EKS(client: client, region: .eucentral2) 41 | case "ap-northeast-1": eks = EKS(client: client, region: .apnortheast1) 42 | case "ap-northeast-2": eks = EKS(client: client, region: .apnortheast2) 43 | case "ap-northeast-3": eks = EKS(client: client, region: .apnortheast3) 44 | case "ap-southeast-1": eks = EKS(client: client, region: .apsoutheast1) 45 | case "ap-southeast-2": eks = EKS(client: client, region: .apsoutheast2) 46 | case "ap-southeast-3": eks = EKS(client: client, region: .apsoutheast3) 47 | case "ap-south-1": eks = EKS(client: client, region: .apsouth1) 48 | case "ap-south-2": eks = EKS(client: client, region: .apsouth2) 49 | case "sa-east-1": eks = EKS(client: client, region: .saeast1) 50 | default: break 51 | } 52 | let r = EKS.DescribeClusterRequest(name: clusterName) 53 | if let eks = eks { 54 | let c = try? eks.describeCluster(r).wait().cluster 55 | let server = c?.endpoint 56 | let ca = c?.certificateAuthority?.data 57 | try? client.syncShutdown() 58 | if let c = c,let server = server,let ca = ca { 59 | return AWSCluster(sever: server, ca: ca) 60 | } else { 61 | return nil 62 | } 63 | 64 | } else { 65 | return nil 66 | } 67 | 68 | } 69 | 70 | func getToken() -> String? { 71 | let client = AWSClient( 72 | credentialProvider: .static(accessKeyId: ak, secretAccessKey: sk), 73 | httpClientProvider: .createNew 74 | ) 75 | var sts: STS? 76 | switch region { 77 | case "us-west-1": sts = STS(client: client, region: .uswest1) 78 | case "us-east-1": sts = STS(client: client, region: .useast1) 79 | case "us-east-2": sts = STS(client: client, region: .useast2) 80 | case "us-west-2": sts = STS(client: client, region: .uswest2) 81 | case "ca-central-1": sts = STS(client: client, region: .cacentral1) 82 | case "eu-west-1": sts = STS(client: client, region: .euwest1) 83 | case "eu-west-2": sts = STS(client: client, region: .euwest2) 84 | case "eu-west-3": sts = STS(client: client, region: .euwest3) 85 | case "eu-south-1": sts = STS(client: client, region: .eusouth1) 86 | case "eu-south-2": sts = STS(client: client, region: .eusouth2) 87 | case "eu-central-1": sts = STS(client: client, region: .eucentral1) 88 | case "eu-central-2": sts = STS(client: client, region: .eucentral2) 89 | case "ap-northeast-1": sts = STS(client: client, region: .apnortheast1) 90 | case "ap-northeast-2": sts = STS(client: client, region: .apnortheast2) 91 | case "ap-northeast-3": sts = STS(client: client, region: .apnortheast3) 92 | case "ap-southeast-1": sts = STS(client: client, region: .apsoutheast1) 93 | case "ap-southeast-2": sts = STS(client: client, region: .apsoutheast2) 94 | case "ap-southeast-3": sts = STS(client: client, region: .apsoutheast3) 95 | case "ap-south-1": sts = STS(client: client, region: .apsouth1) 96 | case "ap-south-2": sts = STS(client: client, region: .apsouth2) 97 | case "sa-east-1": sts = STS(client: client, region: .saeast1) 98 | default: break 99 | } 100 | if let sts = sts,let stsEndpoint = URL(string: sts.endpoint+"/?Action=GetCallerIdentity&Version=2011-06-15") { 101 | let url = try? sts.signURL( 102 | url: stsEndpoint, 103 | httpMethod: .GET, 104 | headers: ["x-k8s-aws-id": clusterName], 105 | expires: .seconds(60) 106 | ).wait() 107 | // print("signed: \(url.absoluteString)") 108 | if let url = url, let tokenSurfix = Base64FS.encodeString(str: url.absoluteString) { 109 | var token = MyAWSClient.TOKEN_PREFIX + tokenSurfix 110 | token.remove(at: token.index(before: token.endIndex)) 111 | token.remove(at: token.index(before: token.endIndex)) 112 | // print("sts: \(token)") 113 | try? client.syncShutdown() 114 | return token 115 | } else { 116 | return nil 117 | } 118 | } else { 119 | return nil 120 | } 121 | } 122 | } 123 | 124 | struct AWSCluster { 125 | let sever: String 126 | let ca: String 127 | } 128 | -------------------------------------------------------------------------------- /K8sCat/Ex.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Ex.swift 3 | // K8sCat 4 | // 5 | // Created by 顾艳华 on 2022/12/20. 6 | // 7 | 8 | import SwiftUI 9 | import SwiftkubeClient 10 | import SwiftkubeModel 11 | 12 | protocol Yamlble { 13 | func encodeYaml(client: KubernetesClient?) async -> String 14 | 15 | func decodeYamlAndUpdate(client: KubernetesClient?, yaml: String) async 16 | } 17 | 18 | extension Bundle { 19 | var releaseVersionNumber: String? { 20 | return infoDictionary?["CFBundleShortVersionString"] as? String 21 | } 22 | var buildVersionNumber: String? { 23 | return infoDictionary?["CFBundleVersion"] as? String 24 | } 25 | } 26 | 27 | extension View { 28 | func border(width: CGFloat, edges: [Edge], color: SwiftUI.Color) -> some View { 29 | overlay(EdgeBorder(width: width, edges: edges).foregroundColor(color)) 30 | } 31 | } 32 | 33 | struct Hpa: Identifiable, Yamlble { 34 | var id: String 35 | var name: String 36 | // let k8sName: String 37 | // let labels: [String: String]? 38 | // let annotations: [String: String]? 39 | let namespace: String 40 | let reference: String 41 | let referenceType: HPAReference 42 | let raw: autoscaling.v2.HorizontalPodAutoscaler? 43 | } 44 | 45 | struct Pod: Identifiable { 46 | var id: String 47 | var name: String 48 | let k8sName: String 49 | let status: String 50 | let expect: Int 51 | let error: Int // container other status 52 | let notReady: Int 53 | let containers: [Container] 54 | let clusterIP: String 55 | let nodeIP: String 56 | let labels: [String: String]? 57 | let annotations: [String: String]? 58 | let namespace: String 59 | let controllerType: PodControllerType 60 | let controllerName: String 61 | let raw: core.v1.Pod? 62 | } 63 | 64 | struct Container: Identifiable { 65 | var id: String 66 | var name: String 67 | let image: String 68 | 69 | let path: String 70 | let policy: String 71 | 72 | let pullPolicy: String 73 | let status: ContainerStatus 74 | let ready: Bool 75 | let error: Bool 76 | } 77 | 78 | struct Deployment: Identifiable, Yamlble { 79 | var id: String 80 | var name: String 81 | let k8sName: [String: String] 82 | // let status: String 83 | let expect: Int 84 | let unavailable: Int 85 | let labels: [String: String]? 86 | let annotations: [String: String]? 87 | let namespace: String 88 | let status: Bool 89 | let raw: apps.v1.Deployment? 90 | } 91 | 92 | struct PersistentVolume: Identifiable, Yamlble { 93 | var id: String 94 | var name: String 95 | let labels: [String: String]? 96 | let annotations: [String: String]? 97 | 98 | // let capactiy: String 99 | let accessModes: String 100 | // let reclaim: String 101 | let status: String 102 | let storageClass: String 103 | let raw: core.v1.PersistentVolume? 104 | } 105 | 106 | struct PersistentVolumeClaim: Identifiable { 107 | var id: String 108 | var name: String 109 | // let labels: [String: String]? 110 | // let annotations: [String: String]? 111 | } 112 | 113 | struct Job: Identifiable { 114 | var id: String 115 | var name: String 116 | let k8sName: [String: String] 117 | let labels: [String: String]? 118 | let annotations: [String: String]? 119 | let namespace: String 120 | let status: Bool 121 | } 122 | 123 | struct CronJob: Identifiable, Yamlble { 124 | var id: String 125 | var name: String 126 | let k8sName: [String: String] 127 | let labels: [String: String]? 128 | let annotations: [String: String]? 129 | let namespace: String 130 | let schedule: String 131 | let raw: batch.v1.CronJob? 132 | } 133 | 134 | struct Stateful: Identifiable, Yamlble { 135 | var id: String 136 | var name: String 137 | let k8sName: [String: String] 138 | let labels: [String: String]? 139 | let annotations: [String: String]? 140 | let namespace: String 141 | let status: Bool 142 | let raw: apps.v1.StatefulSet? 143 | } 144 | 145 | struct Service: Identifiable { 146 | var id: String 147 | var name: String 148 | let k8sName: [String: String] 149 | let type: String 150 | let clusterIps: [String]? 151 | let externalIps: [String]? 152 | let labels: [String: String]? 153 | let annotations: [String: String]? 154 | let namespace: String 155 | // let ports: Int 156 | } 157 | 158 | struct ConfigMap: Identifiable { 159 | var id: String 160 | var name: String 161 | let labels: [String: String]? 162 | let annotations: [String: String]? 163 | let namespace: String 164 | let data: [String: String]? 165 | } 166 | 167 | struct Secret: Identifiable { 168 | var id: String 169 | var name: String 170 | let labels: [String: String]? 171 | let annotations: [String: String]? 172 | let namespace: String 173 | let data: [String: String]? 174 | } 175 | 176 | struct Daemon: Identifiable, Yamlble { 177 | var id: String 178 | var name: String 179 | let k8sName: [String: String] 180 | let labels: [String: String]? 181 | let annotations: [String: String]? 182 | let namespace: String 183 | let status: Bool 184 | let raw: apps.v1.DaemonSet? 185 | } 186 | 187 | struct Replica: Identifiable { 188 | var id: String 189 | var name: String 190 | var k8sName: [String: String] 191 | let labels: [String: String]? 192 | let annotations: [String: String]? 193 | let namespace: String 194 | let status: Bool 195 | } 196 | 197 | //struct Replication: Identifiable { 198 | // var id: String 199 | // var name: String 200 | //} 201 | struct Node: Identifiable { 202 | var id: String 203 | var name: String 204 | var hostName: String 205 | var arch: String 206 | var os: String 207 | 208 | let labels: [String: String]? 209 | let annotations: [String: String]? 210 | 211 | let etcd: Bool 212 | let worker: Bool 213 | let controlPlane: Bool 214 | let agent: Bool 215 | var version: String 216 | 217 | // var age: String 218 | } 219 | -------------------------------------------------------------------------------- /K8sCat/Model+Controller.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Model+Name.swift 3 | // K8sCat 4 | // 5 | // Created by 顾艳华 on 2023/1/11. 6 | // 7 | 8 | import Foundation 9 | import SwiftkubeModel 10 | 11 | extension Model { 12 | mutating func daemonByName(ns: String, name: String) async -> Daemon { 13 | checkAWSToken() 14 | if let client = client { 15 | let daemonOrNil = try? await client.appsV1.daemonSets.get(in: .namespace(ns), name: name) 16 | if let daemon = daemonOrNil { 17 | return Daemon(id: daemon.name ?? "", name: daemon.name ?? "", k8sName: (daemon.spec?.selector.matchLabels) ?? [:] 18 | , labels: daemon.metadata?.labels 19 | , annotations: daemon.metadata?.annotations 20 | , namespace: daemon.metadata?.namespace ?? "unknow" 21 | , status: !(daemon.status?.numberMisscheduled ?? 0 > 0) 22 | , raw: daemon) 23 | } else { 24 | return Daemon(id: "demo", name: "demo", k8sName: [:], labels: [:], annotations: [:], namespace: "demo", status: true, raw: nil) 25 | } 26 | 27 | } else { 28 | return Daemon(id: "demo", name: "demo", k8sName: [:], labels: [:], annotations: [:], namespace: "demo", status: true, raw: nil) 29 | } 30 | } 31 | 32 | mutating func replicaByName(ns: String, name: String) async -> Replica { 33 | checkAWSToken() 34 | if let client = client { 35 | let replicaOrNil = try? await client.appsV1.replicaSets.get(in: .namespace(ns), name: name) 36 | if let replica = replicaOrNil { 37 | return Replica(id: replica.name ?? "", name: replica.name ?? "", k8sName: (replica.spec?.selector.matchLabels) ?? [:] 38 | , labels: replica.metadata?.labels 39 | , annotations: replica.metadata?.annotations 40 | , namespace: replica.metadata?.namespace ?? "unknow" 41 | , status: replica.status?.replicas == replica.status?.readyReplicas 42 | ) 43 | } else { 44 | return Replica(id: "demo", name: "demo", k8sName: [:], labels: [:], annotations: [:], namespace: "demo", status: true) 45 | } 46 | } else { 47 | return Replica(id: "demo", name: "demo", k8sName: [:], labels: [:], annotations: [:], namespace: "demo", status: true) 48 | } 49 | } 50 | 51 | mutating func statefulByName(ns: String, name: String) async -> Stateful { 52 | checkAWSToken() 53 | if let client = client { 54 | let statefulOrNil = try? await client.appsV1.statefulSets.get(in: .namespace(ns), name: name) 55 | if let stateful = statefulOrNil { 56 | return Stateful(id: stateful.name ?? "", name: stateful.name ?? "", k8sName: (stateful.spec?.selector.matchLabels) ?? [:] 57 | , labels: stateful.metadata?.labels 58 | , annotations: stateful.metadata?.annotations 59 | , namespace: stateful.metadata?.namespace ?? "unknow" 60 | , status: stateful.status?.readyReplicas == stateful.status?.replicas, raw: stateful 61 | ) 62 | } else { 63 | return Stateful(id: "demo", name: "demo", k8sName: [:], labels: [:], annotations: [:], namespace: "demo", status: false, raw: nil) 64 | } 65 | } else { 66 | return Stateful(id: "demo", name: "demo", k8sName: [:], labels: [:], annotations: [:], namespace: "demo", status: false, raw: nil) 67 | } 68 | } 69 | 70 | mutating func jobByName(ns: String, name: String) async -> Job { 71 | checkAWSToken() 72 | if let client = client { 73 | let jobOrNil = try? await client.batchV1.jobs.get(in: .namespace(ns), name: name) 74 | if let job = jobOrNil { 75 | return Job(id: job.name ?? "", name: job.name ?? "", 76 | k8sName: (job.spec?.selector?.matchLabels) ?? [:] 77 | , labels: job.metadata?.labels 78 | , annotations: job.metadata?.annotations 79 | , namespace: job.metadata?.namespace ?? "unknow" 80 | , status: job.status?.succeeded != nil 81 | ) 82 | } else { 83 | return Job(id: "demo", name: "demo", k8sName: [:], labels: [:], annotations: [:], namespace: "demo", status: true) 84 | } 85 | } else { 86 | return Job(id: "demo", name: "demo", k8sName: [:], labels: [:], annotations: [:], namespace: "demo", status: true) 87 | } 88 | } 89 | 90 | mutating func deploymentByName(ns: String, name: String) async -> Deployment { 91 | checkAWSToken() 92 | if let client = client { 93 | let deploymentOrNil = try? await client.appsV1.deployments.get(in: .namespace(ns), name: name) 94 | if let deployment = deploymentOrNil { 95 | return Deployment(id: deployment.name ?? "", name: deployment.name ?? "", k8sName: (deployment.spec?.selector.matchLabels) ?? [:], expect: Int(deployment.spec?.replicas ?? 0), unavailable: Int(deployment.status?.unavailableReplicas ?? 0) 96 | , labels: deployment.metadata?.labels 97 | , annotations: deployment.metadata?.annotations 98 | , namespace: deployment.metadata?.namespace ?? "unknow" 99 | , status: deployment.status?.replicas == deployment.status?.readyReplicas 100 | , raw: deployment 101 | ) 102 | } else { 103 | return Deployment(id: "demo1", name: "demo1", k8sName: [:], expect: 2, unavailable: 0, labels: [:], annotations: [:], namespace: "demo1", status: true, raw: nil) 104 | } 105 | } else { 106 | return Deployment(id: "demo1", name: "demo1", k8sName: [:], expect: 2, unavailable: 0, labels: [:], annotations: [:], namespace: "demo1", status: true, raw: nil) 107 | } 108 | } 109 | } 110 | 111 | 112 | -------------------------------------------------------------------------------- /K8sCat.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "async-http-client", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/swift-server/async-http-client.git", 7 | "state" : { 8 | "revision" : "291438696abdd48d2a83b52465c176efbd94512b", 9 | "version" : "1.20.1" 10 | } 11 | }, 12 | { 13 | "identity" : "client", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/swiftkube/client", 16 | "state" : { 17 | "revision" : "2787e28749b8e2a2c67f898984c2dbfe8553390f", 18 | "version" : "0.17.0" 19 | } 20 | }, 21 | { 22 | "identity" : "jmespath.swift", 23 | "kind" : "remoteSourceControl", 24 | "location" : "https://github.com/adam-fowler/jmespath.swift.git", 25 | "state" : { 26 | "revision" : "4513d319c4aaa6c3b2ac18e1e6566a803515ad91", 27 | "version" : "1.0.2" 28 | } 29 | }, 30 | { 31 | "identity" : "model", 32 | "kind" : "remoteSourceControl", 33 | "location" : "https://github.com/swiftkube/model.git", 34 | "state" : { 35 | "revision" : "990eed2d25770ad8bc14ef2a74ffc5dc179a41f7", 36 | "version" : "0.13.0" 37 | } 38 | }, 39 | { 40 | "identity" : "soto", 41 | "kind" : "remoteSourceControl", 42 | "location" : "https://github.com/soto-project/soto", 43 | "state" : { 44 | "revision" : "258094b83797fb9ad4fe3882b1a6a46a92d2c2ae", 45 | "version" : "6.4.0" 46 | } 47 | }, 48 | { 49 | "identity" : "soto-core", 50 | "kind" : "remoteSourceControl", 51 | "location" : "https://github.com/soto-project/soto-core.git", 52 | "state" : { 53 | "revision" : "02d3054ed054a587ce1778bd946a783abd8f62e3", 54 | "version" : "6.4.0" 55 | } 56 | }, 57 | { 58 | "identity" : "swift-algorithms", 59 | "kind" : "remoteSourceControl", 60 | "location" : "https://github.com/apple/swift-algorithms", 61 | "state" : { 62 | "revision" : "f6919dfc309e7f1b56224378b11e28bab5bccc42", 63 | "version" : "1.2.0" 64 | } 65 | }, 66 | { 67 | "identity" : "swift-atomics", 68 | "kind" : "remoteSourceControl", 69 | "location" : "https://github.com/apple/swift-atomics.git", 70 | "state" : { 71 | "revision" : "cd142fd2f64be2100422d658e7411e39489da985", 72 | "version" : "1.2.0" 73 | } 74 | }, 75 | { 76 | "identity" : "swift-collections", 77 | "kind" : "remoteSourceControl", 78 | "location" : "https://github.com/apple/swift-collections.git", 79 | "state" : { 80 | "revision" : "937e904258d22af6e447a0b72c0bc67583ef64a2", 81 | "version" : "1.0.4" 82 | } 83 | }, 84 | { 85 | "identity" : "swift-log", 86 | "kind" : "remoteSourceControl", 87 | "location" : "https://github.com/apple/swift-log.git", 88 | "state" : { 89 | "revision" : "e97a6fcb1ab07462881ac165fdbb37f067e205d5", 90 | "version" : "1.5.4" 91 | } 92 | }, 93 | { 94 | "identity" : "swift-metrics", 95 | "kind" : "remoteSourceControl", 96 | "location" : "https://github.com/apple/swift-metrics.git", 97 | "state" : { 98 | "revision" : "971ba26378ab69c43737ee7ba967a896cb74c0d1", 99 | "version" : "2.4.1" 100 | } 101 | }, 102 | { 103 | "identity" : "swift-nio", 104 | "kind" : "remoteSourceControl", 105 | "location" : "https://github.com/apple/swift-nio", 106 | "state" : { 107 | "revision" : "635b2589494c97e48c62514bc8b37ced762e0a62", 108 | "version" : "2.63.0" 109 | } 110 | }, 111 | { 112 | "identity" : "swift-nio-extras", 113 | "kind" : "remoteSourceControl", 114 | "location" : "https://github.com/apple/swift-nio-extras.git", 115 | "state" : { 116 | "revision" : "91dd2d61fb772e1311bb5f13b59266b579d77e42", 117 | "version" : "1.15.0" 118 | } 119 | }, 120 | { 121 | "identity" : "swift-nio-http2", 122 | "kind" : "remoteSourceControl", 123 | "location" : "https://github.com/apple/swift-nio-http2.git", 124 | "state" : { 125 | "revision" : "d6656967f33ed8b368b38e4b198631fc7c484a40", 126 | "version" : "1.23.1" 127 | } 128 | }, 129 | { 130 | "identity" : "swift-nio-ssl", 131 | "kind" : "remoteSourceControl", 132 | "location" : "https://github.com/apple/swift-nio-ssl.git", 133 | "state" : { 134 | "revision" : "4fb7ead803e38949eb1d6fabb849206a72c580f3", 135 | "version" : "2.23.0" 136 | } 137 | }, 138 | { 139 | "identity" : "swift-nio-transport-services", 140 | "kind" : "remoteSourceControl", 141 | "location" : "https://github.com/apple/swift-nio-transport-services.git", 142 | "state" : { 143 | "revision" : "6cbe0ed2b394f21ab0d46b9f0c50c6be964968ce", 144 | "version" : "1.20.1" 145 | } 146 | }, 147 | { 148 | "identity" : "swift-numerics", 149 | "kind" : "remoteSourceControl", 150 | "location" : "https://github.com/apple/swift-numerics.git", 151 | "state" : { 152 | "revision" : "0a5bc04095a675662cf24757cc0640aa2204253b", 153 | "version" : "1.0.2" 154 | } 155 | }, 156 | { 157 | "identity" : "swift-system", 158 | "kind" : "remoteSourceControl", 159 | "location" : "https://github.com/apple/swift-system.git", 160 | "state" : { 161 | "revision" : "025bcb1165deab2e20d4eaba79967ce73013f496", 162 | "version" : "1.2.1" 163 | } 164 | }, 165 | { 166 | "identity" : "swiftuix", 167 | "kind" : "remoteSourceControl", 168 | "location" : "https://github.com/SwiftUIX/SwiftUIX", 169 | "state" : { 170 | "revision" : "1c2ba19b91a1541b803f9d1b01b360cae5a752fa", 171 | "version" : "0.1.3" 172 | } 173 | }, 174 | { 175 | "identity" : "yams", 176 | "kind" : "remoteSourceControl", 177 | "location" : "https://github.com/jpsim/Yams.git", 178 | "state" : { 179 | "revision" : "0d9ee7ea8c4ebd4a489ad7a73d5c6cad55d6fed3", 180 | "version" : "5.0.6" 181 | } 182 | } 183 | ], 184 | "version" : 2 185 | } 186 | -------------------------------------------------------------------------------- /K8sCat/Model+Yaml.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Model+Yaml.swift 3 | // K8sCat 4 | // 5 | // Created by 顾艳华 on 2023/1/2. 6 | // 7 | 8 | import Foundation 9 | import Yams 10 | import SwiftkubeModel 11 | import SwiftkubeClient 12 | import SwiftUI 13 | 14 | extension Deployment { 15 | func encodeYaml(client: KubernetesClient?) -> String { 16 | if let _ = client { 17 | let encoder = YAMLEncoder() 18 | 19 | if raw != nil { 20 | let r = try? encoder.encode(raw!) 21 | return r ?? "" 22 | } else { 23 | return "" 24 | } 25 | 26 | } else { 27 | return "" 28 | } 29 | } 30 | 31 | func decodeYamlAndUpdate(client: KubernetesClient?, yaml: String) async { 32 | if let client = client { 33 | let decoder = YAMLDecoder() 34 | let d = try? decoder.decode(apps.v1.Deployment.self, from: yaml) 35 | if d != nil { 36 | let _ = try? await client.appsV1.deployments.update(d!) 37 | } 38 | } 39 | } 40 | } 41 | 42 | extension Stateful { 43 | func encodeYaml(client: KubernetesClient?) -> String { 44 | if let _ = client { 45 | let encoder = YAMLEncoder() 46 | 47 | if raw != nil { 48 | let r = try? encoder.encode(raw!) 49 | return r ?? "" 50 | } else { 51 | return "" 52 | } 53 | } else { 54 | return "" 55 | } 56 | } 57 | 58 | func decodeYamlAndUpdate(client: KubernetesClient?, yaml: String) async { 59 | if let client = client { 60 | let decoder = YAMLDecoder() 61 | let d = try? decoder.decode(apps.v1.StatefulSet.self, from: yaml) 62 | if d != nil { 63 | let _ = try? await client.appsV1.statefulSets.update(d!) 64 | } 65 | } 66 | } 67 | } 68 | 69 | extension Daemon { 70 | func encodeYaml(client: KubernetesClient?) -> String { 71 | if let _ = client { 72 | let encoder = YAMLEncoder() 73 | 74 | if raw != nil { 75 | let r = try? encoder.encode(raw!) 76 | return r ?? "" 77 | } else { 78 | return "" 79 | } 80 | } else { 81 | return "" 82 | } 83 | } 84 | 85 | func decodeYamlAndUpdate(client: KubernetesClient?, yaml: String) async { 86 | if let client = client { 87 | let decoder = YAMLDecoder() 88 | let d = try? decoder.decode(apps.v1.DaemonSet.self, from: yaml) 89 | if d != nil { 90 | let _ = try? await client.appsV1.daemonSets.update(d!) 91 | } 92 | } 93 | } 94 | } 95 | 96 | extension Hpa { 97 | func encodeYaml(client: KubernetesClient?) -> String { 98 | if let _ = client { 99 | let encoder = YAMLEncoder() 100 | 101 | if raw != nil { 102 | let r = try? encoder.encode(raw!) 103 | return r ?? "" 104 | } else { 105 | return "" 106 | } 107 | } else { 108 | return "" 109 | } 110 | } 111 | 112 | func decodeYamlAndUpdate(client: KubernetesClient?, yaml: String) async { 113 | if let client = client { 114 | let decoder = YAMLDecoder() 115 | let d = try? decoder.decode(autoscaling.v2.HorizontalPodAutoscaler.self, from: yaml) 116 | if d != nil { 117 | let _ = try? await client.autoScalingV2.horizontalPodAutoscalers.update(d!) 118 | } 119 | } 120 | } 121 | } 122 | 123 | extension CronJob { 124 | func encodeYaml(client: KubernetesClient?) -> String { 125 | if let _ = client { 126 | let encoder = YAMLEncoder() 127 | 128 | if raw != nil { 129 | let r = try? encoder.encode(raw!) 130 | return r ?? "" 131 | } else { 132 | return "" 133 | } 134 | } else { 135 | return "" 136 | } 137 | } 138 | 139 | func decodeYamlAndUpdate(client: KubernetesClient?, yaml: String) async { 140 | if let client = client { 141 | let decoder = YAMLDecoder() 142 | let d = try? decoder.decode(batch.v1.CronJob.self, from: yaml) 143 | if d != nil { 144 | let _ = try? await client.batchV1.cronJobs.update(d!) 145 | } 146 | } 147 | } 148 | } 149 | 150 | extension PersistentVolume { 151 | func encodeYaml(client: KubernetesClient?) -> String { 152 | if let _ = client { 153 | let encoder = YAMLEncoder() 154 | 155 | if raw != nil { 156 | let r = try? encoder.encode(raw!) 157 | return r ?? "" 158 | } else { 159 | return "" 160 | } 161 | } else { 162 | return "" 163 | } 164 | } 165 | 166 | func decodeYamlAndUpdate(client: KubernetesClient?, yaml: String) async { 167 | if let client = client { 168 | let decoder = YAMLDecoder() 169 | let d = try? decoder.decode(core.v1.PersistentVolume.self, from: yaml) 170 | if d != nil { 171 | let _ = try? await client.persistentVolumes.update(d!) 172 | } 173 | } 174 | } 175 | } 176 | 177 | 178 | 179 | func urlScheme(yamlble: Yamlble, client: KubernetesClient?) async { 180 | let utf8str = await yamlble.encodeYaml(client: client).data(using: .utf8) 181 | if let utf8str = utf8str { 182 | let base64Encoded = utf8str.base64EncodedString(options: Data.Base64EncodingOptions(rawValue: 0)) 183 | #if os(iOS) 184 | if let url = URL(string: "yamler://" + base64Encoded) { 185 | if await UIApplication.shared.canOpenURL(url) { 186 | await UIApplication.shared.open(url) 187 | } else { 188 | if let url = URL(string: "https://apps.apple.com/cn/app/yamler/id1660009640") { 189 | await UIApplication.shared.open(url) 190 | } 191 | } 192 | 193 | } 194 | #endif 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /K8sCat/inner/PodView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PodView.swift 3 | // K8sCat 4 | // 5 | // Created by 顾艳华 on 2022/12/26. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct PodView: View { 11 | @State var daemon: Daemon = Daemon(id: "demo", name: "demo", k8sName: [:], labels: [:], annotations: [:], namespace: "demo", status: true, raw: nil) 12 | @State var replica: Replica = Replica(id: "demo", name: "demo", k8sName: [:], labels: [:], annotations: [:], namespace: "demo", status: true) 13 | @State var stateful = Stateful(id: "demo", name: "demo", k8sName: [:], labels: [:], annotations: [:], namespace: "demo", status: false, raw: nil) 14 | @State var job = Job(id: "demo", name: "demo", k8sName: [:], labels: [:], annotations: [:], namespace: "demo", status: true) 15 | 16 | let pod: Pod 17 | let viewModel: ViewModel 18 | var body: some View { 19 | Form { 20 | Section(header: "Name") { 21 | Text(pod.name) 22 | } 23 | Section(header: "Status") { 24 | Text(pod.status) 25 | } 26 | Section(header: "Containers") { 27 | List { 28 | ForEach(pod.containers) { 29 | c in 30 | NavigationLink { 31 | ContainerView(pod: pod, container: c, viewModel: viewModel) 32 | } label: { 33 | Image(systemName: "tray.full") 34 | VStack(alignment: .leading) { 35 | Text(c.name) 36 | .foregroundColor(c.ready ? .green : (c.error ? .red : .yellow)) 37 | CaptionText(text: c.image) 38 | } 39 | } 40 | 41 | 42 | } 43 | } 44 | } 45 | Section(header: "Contoller") { 46 | NavigationLink { 47 | switch pod.controllerType { 48 | case .DaemonSet: 49 | DaemonView(daemon: daemon, viewModel: viewModel) 50 | .task { 51 | daemon = await viewModel.model.daemonByName(ns: viewModel.ns, name: pod.controllerName) 52 | } 53 | case .ReplicaSet: 54 | ReplicaView(replica: replica, viewModel: viewModel) 55 | .task { 56 | replica = await viewModel.model.replicaByName(ns: viewModel.ns, name: pod.controllerName) 57 | } 58 | case .StatefulSet: 59 | StatefulView(stateful: stateful, viewModel: viewModel) 60 | .task { 61 | stateful = await viewModel.model.statefulByName(ns: viewModel.ns, name: pod.controllerName) 62 | } 63 | case .Job: 64 | JobView(job: job, viewModel: viewModel) 65 | .task { 66 | job = await viewModel.model.jobByName(ns: viewModel.ns, name: pod.controllerName) 67 | } 68 | default: EmptyView() 69 | } 70 | } label: { 71 | switch pod.controllerType { 72 | case .DaemonSet: 73 | Image(systemName: "xserve") 74 | case .ReplicaSet: 75 | Image(systemName: "square.3.layers.3d.down.left") 76 | case .StatefulSet: 77 | Image(systemName: "macpro.gen2.fill") 78 | case .Job: 79 | Image(systemName: "figure.run") 80 | default: EmptyView() 81 | } 82 | Text(pod.controllerName) 83 | } 84 | } 85 | Section(header: "Labels and Annotations") { 86 | NavigationLink { 87 | List { 88 | ForEach((pod.labels ?? [:]).sorted(by: >), id: \.key) { 89 | key, value in 90 | VStack(alignment: .leading) { 91 | Text(key) 92 | 93 | CaptionText(text: value) 94 | } 95 | } 96 | } 97 | 98 | } label: { 99 | Text("Labels") 100 | } 101 | NavigationLink { 102 | List { 103 | ForEach((pod.annotations ?? [:]).sorted(by: >), id: \.key) { 104 | key, value in 105 | VStack(alignment: .leading) { 106 | Text(key) 107 | CaptionText(text: value) 108 | } 109 | } 110 | } 111 | } label: { 112 | Text("Annotations") 113 | } 114 | } 115 | Section(header: "Ip") { 116 | HStack{ 117 | Text("Pod IP") 118 | Spacer() 119 | Text(pod.clusterIP) 120 | } 121 | HStack{ 122 | Text("Node IP") 123 | Spacer() 124 | Text(pod.nodeIP) 125 | } 126 | } 127 | Section(header: "Misc") { 128 | HStack{ 129 | Text("Namespace") 130 | Spacer() 131 | Text(pod.namespace) 132 | } 133 | 134 | } 135 | } 136 | .navigationTitle("Pod") 137 | .onAppear { 138 | viewModel.model.checkAWSToken() 139 | } 140 | } 141 | } 142 | 143 | //struct PodView_Previews: PreviewProvider { 144 | // static var previews: some View { 145 | // PodView(pod: Pod(id: "123", name: "123", k8sName: "123", status: "fail", expect: 8, warning: 7, containers: [Container(id: "abc", name: "abclong....", image: "hello",path: "/",policy: "r",pullPolicy: "r"), Container(id: "ef", name: "ef", image: "kkk", path: "/", policy: "r", pullPolicy: "r")],clusterIP: "10.0.0.3", nodeIP: "192.168.1.3", labels: ["l1":"l1v"],annotations: ["a1":"a1v"],namespace: "default", raw: nil), viewModel: ViewModel(viewContext: PersistenceController.preview.container.viewContext)) 146 | // } 147 | //} 148 | 149 | enum PodStatus: String { 150 | case Pending 151 | case Running 152 | case Succeeded 153 | case Failed 154 | case Unknown 155 | } 156 | 157 | enum PodControllerType: String { 158 | case ReplicaSet 159 | case Job 160 | case StatefulSet 161 | case DaemonSet 162 | case UnKonw 163 | } 164 | -------------------------------------------------------------------------------- /K8sCat/inner/DeploymentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DeploymentView.swift 3 | // K8sCat 4 | // 5 | // Created by 顾艳华 on 2022/12/26. 6 | // 7 | 8 | import SwiftUI 9 | // deployment has child 10 | struct DeploymentView: View { 11 | let deployment: Deployment? 12 | let viewModel: ViewModel 13 | 14 | @State var pods: [Pod] = [] 15 | @State var replicas: [Replica] = [] 16 | 17 | @State var showYaml = false 18 | var body: some View { 19 | if let deployment = deployment { 20 | Form { 21 | Section(header: "Name") { 22 | Text(deployment.name) 23 | } 24 | Section(header: "Status") { 25 | Text(deployment.status ? "Ready" : "Failed") 26 | } 27 | // Button{ 28 | // viewModel.model.scaleDeployment(deployment: deployment, replicas: 1) 29 | // } label: { 30 | // Text("scale") 31 | // } 32 | Section(header: "Replica Sets") { 33 | List { 34 | ForEach(replicas) { 35 | i in 36 | NavigationLink { 37 | ReplicaView(replica: i, viewModel: viewModel) 38 | } label: { 39 | Image(systemName: "square.3.layers.3d.down.left") 40 | Text(i.name) 41 | .foregroundColor(i.status ? .green : .red) 42 | } 43 | } 44 | } 45 | } 46 | .task { 47 | replicas = await viewModel.model.replicaByDeployment(in: .namespace(viewModel.ns), deployment: deployment.k8sName) 48 | } 49 | Section(header: "Pods") { 50 | List { 51 | ForEach(pods) { 52 | i in 53 | NavigationLink { 54 | PodView(pod: i, viewModel: viewModel) 55 | } label: { 56 | Image(systemName: "tray.2") 57 | VStack(alignment: .leading) { 58 | Text(i.name) 59 | HStack{ 60 | Text("containers -").font(.caption) 61 | CaptionText(text: "expect: \(i.expect), ") 62 | Text("error: \(i.status == PodStatus.Succeeded.rawValue ? 0 : i.error)").font(.caption).foregroundColor((i.status == PodStatus.Succeeded.rawValue ? 0 : i.error) > 0 ? .red : .none) 63 | Text("not ready: \(i.status == PodStatus.Succeeded.rawValue ? 0 :i.notReady)").font(.caption).foregroundColor((i.status == PodStatus.Succeeded.rawValue ? 0 : i.notReady) > 0 ? .yellow : .none) 64 | } 65 | 66 | } 67 | } 68 | } 69 | 70 | } 71 | } 72 | .task { 73 | pods = await viewModel.model.podsByDeployment(in: .namespace(viewModel.ns), deployment: deployment.k8sName) 74 | } 75 | Section(header: "Labels and Annotations") { 76 | NavigationLink { 77 | List { 78 | ForEach((deployment.labels ?? [:]).sorted(by: >), id: \.key) { 79 | key, value in 80 | VStack(alignment: .leading) { 81 | Text(key) 82 | 83 | CaptionText(text: value) 84 | } 85 | } 86 | } 87 | 88 | } label: { 89 | Text("Labels") 90 | } 91 | NavigationLink { 92 | List { 93 | ForEach((deployment.annotations ?? [:]).sorted(by: >), id: \.key) { 94 | key, value in 95 | VStack(alignment: .leading) { 96 | Text(key) 97 | CaptionText(text: value) 98 | } 99 | } 100 | } 101 | } label: { 102 | Text("Annotations") 103 | } 104 | } 105 | 106 | Section(header: "Misc") { 107 | HStack{ 108 | Text("Namespace") 109 | Spacer() 110 | Text(deployment.namespace) 111 | } 112 | 113 | } 114 | }.toolbar{ 115 | Menu { 116 | Button { 117 | // do something 118 | // let yaml = deployment.encodeYaml(client: viewModel.model.client) 119 | // print("Yaml: \(yaml)") 120 | showYaml = true 121 | // deployment.decodeYaml(client: viewModel.model.client, yaml: yaml) 122 | } label: { 123 | Text("View/Edit Yaml") 124 | Image(systemName: "note.text") 125 | } 126 | // Button { 127 | // // do something 128 | // } label: { 129 | // Text("Delete Resource") 130 | // Image(systemName: "trash") 131 | // } 132 | } label: { 133 | Image(systemName: "ellipsis") 134 | } 135 | }.sheet(isPresented: $showYaml){ 136 | #if os(iOS) 137 | YamlWebView(yamlble: deployment, model: viewModel.model) { 138 | showYaml = false 139 | } 140 | #endif 141 | Button{ 142 | Task { 143 | await urlScheme(yamlble: deployment, client: viewModel.model.client) 144 | } 145 | }label: { 146 | Text("Load yaml via Yamler") 147 | }.padding() 148 | } 149 | .navigationTitle("Deployment") 150 | } else { 151 | EmptyView() 152 | } 153 | } 154 | } 155 | 156 | struct DeploymentView_Previews: PreviewProvider { 157 | static var previews: some View { 158 | DeploymentView(deployment: Deployment(id: "123", name: "123", k8sName: [:], expect: 1, unavailable: 0, labels: ["l1":"l1v"],annotations: ["a1":"a1v"],namespace: "default", status: false, raw: nil), viewModel: ViewModel(viewContext: PersistenceController.preview.container.viewContext)) 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /K8sCat/id/Utils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Token.swift 3 | // K8sCat 4 | // 5 | // Created by 顾艳华 on 2023/1/7. 6 | // 7 | 8 | import Foundation 9 | 10 | public class Base64FS { 11 | 12 | private static let padding: UInt8 = 61 // Padding = "=" 13 | 14 | private static let filenameSafeAlphabet: [UInt8] = [ 15 | 65, // 0 = "A" 16 | 66, // 1 = "B" 17 | 67, // 2 = "C" 18 | 68, // 3 = "D" 19 | 69, // 4 = "E" 20 | 70, // 5 = "F" 21 | 71, // 6 = "G" 22 | 72, // 7 = "H" 23 | 73, // 8 = "I" 24 | 74, // 9 = "J" 25 | 75, // 10 = "K" 26 | 76, // 11 = "L" 27 | 77, // 12 = "M" 28 | 78, // 13 = "N" 29 | 79, // 14 = "O" 30 | 80, // 15 = "P" 31 | 81, // 16 = "Q" 32 | 82, // 17 = "R" 33 | 83, // 18 = "S" 34 | 84, // 19 = "T" 35 | 85, // 20 = "U" 36 | 86, // 21 = "V" 37 | 87, // 22 = "W" 38 | 88, // 23 = "X" 39 | 89, // 24 = "Y" 40 | 90, // 25 = "Z" 41 | 97, // 26 = "a" 42 | 98, // 27 = "b" 43 | 99, // 28 = "c" 44 | 100, // 29 = "d" 45 | 101, // 30 = "e" 46 | 102, // 31 = "f" 47 | 103, // 32 = "g" 48 | 104, // 33 = "h" 49 | 105, // 34 = "i" 50 | 106, // 35 = "j" 51 | 107, // 36 = "k" 52 | 108, // 37 = "l" 53 | 109, // 38 = "m" 54 | 110, // 39 = "n" 55 | 111, // 40 = "o" 56 | 112, // 41 = "p" 57 | 113, // 42 = "q" 58 | 114, // 43 = "r" 59 | 115, // 44 = "s" 60 | 116, // 45 = "t" 61 | 117, // 46 = "u" 62 | 118, // 47 = "v" 63 | 119, // 48 = "w" 64 | 120, // 49 = "x" 65 | 121, // 50 = "y" 66 | 122, // 51 = "z" 67 | 48, // 52 = "0" 68 | 49, // 53 = "1" 69 | 50, // 54 = "2" 70 | 51, // 55 = "3" 71 | 52, // 56 = "4" 72 | 53, // 57 = "5" 73 | 54, // 58 = "6" 74 | 55, // 59 = "7" 75 | 56, // 60 = "8" 76 | 57, // 61 = "9" 77 | 45, // 62 = "-" 78 | 95, // 63 = "_" 79 | ] 80 | 81 | private static let safeAlphabetToIndex: [UInt8 : UInt8] = [ 82 | 61 : 0, // Padding = 0 83 | 65 : 0, // 0 = "A" 84 | 66 : 1, // 1 = "B" 85 | 67 : 2, // 2 = "C" 86 | 68 : 3, // 3 = "D" 87 | 69 : 4, // 4 = "E" 88 | 70 : 5, // 5 = "F" 89 | 71 : 6, // 6 = "G" 90 | 72 : 7, // 7 = "H" 91 | 73 : 8, // 8 = "I" 92 | 74 : 9, // 9 = "J" 93 | 75 : 10, // 10 = "K" 94 | 76 : 11, // 11 = "L" 95 | 77 : 12, // 12 = "M" 96 | 78 : 13, // 13 = "N" 97 | 79 : 14, // 14 = "O" 98 | 80 : 15, // 15 = "P" 99 | 81 : 16, // 16 = "Q" 100 | 82 : 17, // 17 = "R" 101 | 83 : 18, // 18 = "S" 102 | 84 : 19, // 19 = "T" 103 | 85 : 20, // 20 = "U" 104 | 86 : 21, // 21 = "V" 105 | 87 : 22, // 22 = "W" 106 | 88 : 23, // 23 = "X" 107 | 89 : 24, // 24 = "Y" 108 | 90 : 25, // 25 = "Z" 109 | 97 : 26, // 26 = "a" 110 | 98 : 27, // 27 = "b" 111 | 99 : 28, // 28 = "c" 112 | 100 : 29, // 29 = "d" 113 | 101 : 30, // 30 = "e" 114 | 102 : 31, // 31 = "f" 115 | 103 : 32, // 32 = "g" 116 | 104 : 33, // 33 = "h" 117 | 105 : 34, // 34 = "i" 118 | 106 : 35, // 35 = "j" 119 | 107 : 36, // 36 = "k" 120 | 108 : 37, // 37 = "l" 121 | 109 : 38, // 38 = "m" 122 | 110 : 39, // 39 = "n" 123 | 111 : 40, // 40 = "o" 124 | 112 : 41, // 41 = "p" 125 | 113 : 42, // 42 = "q" 126 | 114 : 43, // 43 = "r" 127 | 115 : 44, // 44 = "s" 128 | 116 : 45, // 45 = "t" 129 | 117 : 46, // 46 = "u" 130 | 118 : 47, // 47 = "v" 131 | 119 : 48, // 48 = "w" 132 | 120 : 49, // 49 = "x" 133 | 121 : 50, // 50 = "y" 134 | 122 : 51, // 51 = "z" 135 | 48 : 52, // 52 = "0" 136 | 49 : 53, // 53 = "1" 137 | 50 : 54, // 54 = "2" 138 | 51 : 55, // 55 = "3" 139 | 52 : 56, // 56 = "4" 140 | 53 : 57, // 57 = "5" 141 | 54 : 58, // 58 = "6" 142 | 55 : 59, // 59 = "7" 143 | 56 : 60, // 60 = "8" 144 | 57 : 61, // 61 = "9" 145 | 45 : 62, // 62 = "-" 146 | 95 : 63, // 63 = "_" 147 | ] 148 | 149 | 150 | public static func encodeString(str: String) -> String? { 151 | 152 | // Get the ascii representation and return 153 | let data = str.data(using: .ascii) 154 | if let data = data { 155 | let encData = encode(data: [UInt8](data)) 156 | 157 | return String(data: Data(encData), encoding: .ascii) 158 | } else { 159 | return nil 160 | } 161 | } 162 | 163 | public static func encode(data: [UInt8]) -> [UInt8] { 164 | 165 | var result: [UInt8] = [] 166 | 167 | let size = data.count 168 | 169 | // Step through 3 bytes at a time 170 | for i in stride(from: 0, to: size, by: 3) { 171 | 172 | 173 | // Get the first 6 bits, and add the Base64 letter 174 | let first = data[i] >> 2 175 | result.append(filenameSafeAlphabet[Int(first)]) 176 | 177 | // Get the remaining 2 bits from the previous byte 178 | var second = (data[i] & 0b11) << 4 179 | 180 | 181 | // If there is more of the array, add the next 4 bits from byte 2, or return with padding for the 3rd and 4th characters 182 | if i + 1 < size { 183 | second |= (data[i + 1] & 0b11110000) >> 4 184 | result.append(filenameSafeAlphabet[Int(second)]) 185 | } else { 186 | result.append(filenameSafeAlphabet[Int(second)]) 187 | result.append(padding) 188 | result.append(padding) 189 | return result 190 | } 191 | 192 | 193 | // Get the remaining 4 bits from the previous byte 194 | var third = (data[i + 1] & 0b1111) << 2 195 | 196 | // If there is more of the array, add the next 2 bits from byte 3, or return with padding for the 4th character 197 | if i + 2 < size { 198 | third |= (data[i + 2] & 0b11000000) >> 6 199 | result.append(filenameSafeAlphabet[Int(third)]) 200 | } else { 201 | result.append(filenameSafeAlphabet[Int(third)]) 202 | result.append(padding) 203 | return result 204 | } 205 | 206 | 207 | // Get the remaining 6 bits from the previous byte, add to the result 208 | let forth = data[i + 2] & 0b00111111 209 | result.append(filenameSafeAlphabet[Int(forth)]) 210 | } 211 | 212 | return result 213 | } 214 | 215 | public static func decode(data: [UInt8]) -> [UInt8] { 216 | 217 | var result: [UInt8] = [] 218 | 219 | let size = data.count 220 | // We loop over the 4 letters at a time 221 | // We know it is padded, so we dont need to check for size 222 | for i in stride(from: 0, to: size, by: 4) { 223 | 224 | // Get the 4 letters, then get back to their non-index values 225 | let first = safeAlphabetToIndex[data[i]]! 226 | let second = safeAlphabetToIndex[data[i + 1]]! 227 | let third = safeAlphabetToIndex[data[i + 2]]! 228 | let forth = safeAlphabetToIndex[data[i + 3]]! 229 | 230 | // Get the 3 binary letters from the four 6-bit ones 231 | let l1 = first << 2 | ((second & 0b110000) >> 4) 232 | let l2 = ((second & 0b1111) << 4) | ((third & 0b111100) >> 2) 233 | let l3 = ((third & 0b11) << 6) | forth 234 | 235 | // Return the letters if they arent empty 236 | result.append(l1) 237 | 238 | if l3 != 0 { 239 | result.append(l2) 240 | result.append(l3) 241 | } else if l2 != 0 { 242 | result.append(l2) 243 | } 244 | } 245 | 246 | return result 247 | } 248 | 249 | public static func decodeString(str: String) -> String { 250 | 251 | // Get the ascii representation and return 252 | let data = str.data(using: .ascii)! 253 | 254 | let decData = decode(data: [UInt8](data)) 255 | 256 | let retStr = String(data: Data(decData), encoding: .ascii)! 257 | 258 | return retStr 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /K8sCat/cluster/AWSView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConfigView.swift 3 | // K8sCat 4 | // 5 | // Created by 顾艳华 on 2022/12/31. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct AWSView: View { 11 | @Environment(\.managedObjectContext) private var viewContext 12 | let first: Bool 13 | let close: () -> Void 14 | @State var name = "" 15 | @State var icon = "a.circle" 16 | @State var accessKeyID = "" 17 | @State var secretAccessKey = "" 18 | @State var region = "us-west-1" 19 | @State var clusterName = "" 20 | 21 | @State var showHelp = false 22 | @State var showErrorMessage = false 23 | @State var errorMessage = "" 24 | 25 | @State private var showingExporter = false 26 | 27 | var columns = Array(repeating: GridItem(.flexible(), spacing: 10), count: 5) 28 | var body: some View { 29 | NavigationStack { 30 | Form { 31 | 32 | Section(header: "Name"){ 33 | TextField(text: $name) 34 | .disableAutocorrection(true) 35 | .textInputAutocapitalization(.never) 36 | } 37 | Section(header: "Icon"){ 38 | Picker("Icon", selection: $icon){ 39 | ForEach(["a.circle", "b.circle", "c.circle", "e.circle", "f.circle", "g.circle", "h.circle", "i.circle", "j.circle", "k.circle", "l.circle", "m.circle", "n.circle", "o.circle", "p.circle", "q.circle", "r.circle", "s.circle", "t.circle", "u.circle", "v.circle", "w.circle", "x.circle", "y.circle"], id: \.self) { 40 | Image(systemName: $0) 41 | } 42 | 43 | } 44 | 45 | } 46 | Section(header: "Access Key ID"){ 47 | TextField(text: $accessKeyID) 48 | .disableAutocorrection(true) 49 | .textInputAutocapitalization(.never) 50 | } 51 | Section(header: "Secret Access key"){ 52 | TextField(text: $secretAccessKey) 53 | .disableAutocorrection(true) 54 | .textInputAutocapitalization(.never) 55 | } 56 | 57 | Section(header: "Region"){ 58 | Picker("Region", selection: $region){ 59 | ForEach([AWSResign(id: "us-east-1", render: "US East (N. Virginia)", value: "us-east-1") 60 | ,AWSResign(id: "us-east-2", render: "US East (Ohio)", value: "us-east-2") 61 | ,AWSResign(id: "us-west-1", render: "US West (N. California)", value: "us-west-1") 62 | ,AWSResign(id: "us-west-2", render: "US West (Oregon)", value: "us-west-2") 63 | ,AWSResign(id: "ca-central-1", render: "Canada (Central)", value: "ca-central-1") 64 | ,AWSResign(id: "eu-west-1", render: "EU (Ireland)", value: "eu-west-1") 65 | ,AWSResign(id: "eu-central-1", render: "EU (Frankfurt)", value: "eu-central-1") 66 | ,AWSResign(id: "eu-central-2", render: "EU (Zurich)", value: "eu-central-2") 67 | ,AWSResign(id: "eu-west-2", render: "EU (Frankfurt)", value: "eu-west-2") 68 | ,AWSResign(id: "eu-west-3", render: "EU (Paris)", value: "eu-west-3") 69 | ,AWSResign(id: "eu-north-1", render: "EU (Stockholm)", value: "eu-north-1") 70 | ,AWSResign(id: "eu-south-1", render: "EU (Milan)", value: "eu-south-1") 71 | ,AWSResign(id: "eu-south-2", render: "EU (Spain)", value: "eu-south-2") 72 | ,AWSResign(id: "ap-northeast-1", render: "Asia Pacific (Tokyo)", value: "ap-northeast-1") 73 | ,AWSResign(id: "ap-northeast-2", render: "Asia Pacific (Seoul)", value: "ap-northeast-2") 74 | ,AWSResign(id: "ap-northeast-3", render: "Asia Pacific (Osaka-Local)", value: "ap-northeast-3") 75 | ,AWSResign(id: "ap-southeast-1", render: "Asia Pacific (Singapore)", value: "ap-southeast-1") 76 | ,AWSResign(id: "ap-southeast-2", render: "Asia Pacific (Sydney)", value: "ap-southeast-2") 77 | ,AWSResign(id: "ap-southeast-3", render: "Asia Pacific (Jakarta)", value: "ap-southeast-3") 78 | ,AWSResign(id: "ap-south-1", render: "Asia Pacific (Mumbai)", value: "ap-south-1") 79 | ,AWSResign(id: "ap-south-2", render: "Asia Pacific (Hyderabad)", value: "ap-south-2") 80 | ,AWSResign(id: "sa-east-1", render: "South America (São Paulo)", value: "sa-east-1") 81 | ]) { 82 | Text($0.render) 83 | } 84 | 85 | } 86 | } 87 | Section(header: "Cluster Name"){ 88 | TextField(text: $clusterName) 89 | .disableAutocorrection(true) 90 | .textInputAutocapitalization(.never) 91 | } 92 | Button{ 93 | // save to core data 94 | addItem() 95 | 96 | } label: { 97 | Text("Save") 98 | } 99 | } 100 | 101 | } 102 | .alert(errorMessage, isPresented: $showErrorMessage){ 103 | Button("OK", role: .cancel) { 104 | showErrorMessage = false 105 | } 106 | } 107 | .toolbar{ 108 | Button { 109 | showHelp = true 110 | }label: { 111 | Image(systemName: "questionmark.circle") 112 | } 113 | 114 | } 115 | .sheet(isPresented: $showHelp){ 116 | AWSHelpView() 117 | } 118 | 119 | } 120 | 121 | private func addItem() { 122 | if name.isEmpty { 123 | showErrorMessage = true 124 | errorMessage = "Input cluster name, please." 125 | return 126 | } 127 | if accessKeyID.isEmpty { 128 | showErrorMessage = true 129 | errorMessage = "Input AWS access key ID, please." 130 | return 131 | } 132 | if secretAccessKey.isEmpty { 133 | showErrorMessage = true 134 | errorMessage = "Input AWS secret access key, please." 135 | return 136 | } 137 | if region.isEmpty { 138 | showErrorMessage = true 139 | errorMessage = "Input AWS region, please." 140 | return 141 | } 142 | if clusterName.isEmpty { 143 | showErrorMessage = true 144 | errorMessage = "Input AWS EKS cluster name, please." 145 | return 146 | } 147 | // if content.isEmpty { 148 | // showErrorMessage = true 149 | // errorMessage = "Import kube config, please." 150 | // return 151 | // } 152 | withAnimation { 153 | let newItem = ClusterEntry(context: viewContext) 154 | newItem.name = self.name 155 | newItem.type = ClusterType.AWS.rawValue 156 | newItem.icon = icon 157 | newItem.accessKeyID = accessKeyID 158 | newItem.secretAccessKey = secretAccessKey 159 | newItem.region = region 160 | newItem.clusterName = clusterName 161 | 162 | if first { 163 | newItem.selected = true 164 | } 165 | 166 | do { 167 | try viewContext.save() 168 | } catch { 169 | // Replace this implementation with code to handle the error appropriately. 170 | // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. 171 | let nsError = error as NSError 172 | fatalError("Unresolved error \(nsError), \(nsError.userInfo)") 173 | } 174 | } 175 | close() 176 | } 177 | } 178 | 179 | //struct ConfigView_Previews: PreviewProvider { 180 | // static var previews: some View { 181 | // ConfigView(first: true){} 182 | // } 183 | //} 184 | 185 | struct AWSHelpView: View { 186 | var body: some View { 187 | VStack(alignment: .leading){ 188 | Text("Overview").font(.title) 189 | .padding(.vertical) 190 | Text(""" 191 | AWS provides a managed Kubernetes service called Amazon EKS (Amazon Elastic Container Service for Kubernetes) making it easy to deploy, manage, and scale containerized applications using Kubernetes on AWS. It's pretty straightforward to import them to Kuber if you're running Kubernetes clusters with EKS. 192 | 193 | It is recommended to use eksctl to build a cluster, refer to: https://docs.aws.amazon.com/eks/latest/userguide/getting-started-eksctl.html. 194 | 195 | Use the Access key ID and Secret access key of the account used by eksctl. 196 | 197 | Install eksctl: https://docs.aws.amazon.com/eks/latest/userguide/eksctl.html 198 | """) 199 | Text("Select a region").font(.title) 200 | .padding(.vertical) 201 | Text(""" 202 | Each EKS powered Kubernetes belongs to a region, so you need to select a region first. Notice that AWS China regions are not supported at this time 203 | """) 204 | Spacer() 205 | } 206 | .padding() 207 | 208 | 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /K8sCat/yaml/static/js/787.eb3a1d75.chunk.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"static/js/787.eb3a1d75.chunk.js","mappings":"uQAAA,IAAIA,EAAEC,EAAEC,EAAEC,EAAEC,EAAE,SAASJ,EAAEC,GAAG,MAAM,CAACI,KAAKL,EAAEM,WAAM,IAASL,GAAG,EAAEA,EAAEM,MAAM,EAAEC,QAAQ,GAAGC,GAAG,MAAMC,OAAOC,KAAKC,MAAM,KAAKF,OAAOG,KAAKC,MAAM,cAAcD,KAAKE,UAAU,MAAM,EAAEC,EAAE,SAAShB,EAAEC,GAAG,IAAI,GAAGgB,oBAAoBC,oBAAoBC,SAASnB,GAAG,CAAC,GAAG,gBAAgBA,KAAK,2BAA2BoB,MAAM,OAAO,IAAIlB,EAAE,IAAIe,qBAAqB,SAASjB,GAAG,OAAOA,EAAEqB,aAAaC,IAAIrB,EAAE,IAAI,OAAOC,EAAEqB,QAAQ,CAACC,KAAKxB,EAAEyB,UAAS,IAAKvB,CAAC,CAAW,CAAT,MAAMF,GAAG,CAAC,EAAE0B,EAAE,SAAS1B,EAAEC,GAAG,IAAIC,EAAE,SAASA,EAAEC,GAAG,aAAaA,EAAEqB,MAAM,WAAWG,SAASC,kBAAkB5B,EAAEG,GAAGF,IAAI4B,oBAAoB,mBAAmB3B,GAAE,GAAI2B,oBAAoB,WAAW3B,GAAE,IAAK,EAAE4B,iBAAiB,mBAAmB5B,GAAE,GAAI4B,iBAAiB,WAAW5B,GAAE,EAAG,EAAE6B,EAAE,SAAS/B,GAAG8B,iBAAiB,YAAY,SAAS7B,GAAGA,EAAE+B,WAAWhC,EAAEC,EAAE,IAAG,EAAG,EAAEgC,EAAE,SAASjC,EAAEC,EAAEC,GAAG,IAAIC,EAAE,OAAO,SAASC,GAAGH,EAAEK,OAAO,IAAIF,GAAGF,KAAKD,EAAEM,MAAMN,EAAEK,OAAOH,GAAG,IAAIF,EAAEM,YAAO,IAASJ,KAAKA,EAAEF,EAAEK,MAAMN,EAAEC,IAAI,CAAC,EAAEiC,GAAG,EAAEC,EAAE,WAAW,MAAM,WAAWR,SAASC,gBAAgB,EAAE,GAAG,EAAEQ,EAAE,WAAWV,GAAG,SAAS1B,GAAG,IAAIC,EAAED,EAAEqC,UAAUH,EAAEjC,CAAC,IAAG,EAAG,EAAEqC,EAAE,WAAW,OAAOJ,EAAE,IAAIA,EAAEC,IAAIC,IAAIL,GAAG,WAAWQ,YAAY,WAAWL,EAAEC,IAAIC,GAAG,GAAG,EAAE,KAAK,CAAKI,sBAAkB,OAAON,CAAC,EAAE,EAAEO,EAAE,SAASzC,EAAEC,GAAG,IAAIC,EAAEC,EAAEmC,IAAIZ,EAAEtB,EAAE,OAAO8B,EAAE,SAASlC,GAAG,2BAA2BA,EAAEK,OAAO+B,GAAGA,EAAEM,aAAa1C,EAAE2C,UAAUxC,EAAEqC,kBAAkBd,EAAEpB,MAAMN,EAAE2C,UAAUjB,EAAElB,QAAQoC,KAAK5C,GAAGE,GAAE,IAAK,EAAEiC,EAAEU,OAAOC,aAAaA,YAAYC,kBAAkBD,YAAYC,iBAAiB,0BAA0B,GAAGX,EAAED,EAAE,KAAKnB,EAAE,QAAQkB,IAAIC,GAAGC,KAAKlC,EAAE+B,EAAEjC,EAAE0B,EAAEzB,GAAGkC,GAAGD,EAAEC,GAAGJ,GAAG,SAAS5B,GAAGuB,EAAEtB,EAAE,OAAOF,EAAE+B,EAAEjC,EAAE0B,EAAEzB,GAAG+C,uBAAuB,WAAWA,uBAAuB,WAAWtB,EAAEpB,MAAMwC,YAAYlC,MAAMT,EAAEkC,UAAUnC,GAAE,EAAG,GAAG,GAAG,IAAI,EAAE+C,GAAE,EAAGC,GAAG,EAAEC,EAAE,SAASnD,EAAEC,GAAGgD,IAAIR,GAAG,SAASzC,GAAGkD,EAAElD,EAAEM,KAAK,IAAI2C,GAAE,GAAI,IAAI/C,EAAEC,EAAE,SAASF,GAAGiD,GAAG,GAAGlD,EAAEC,EAAE,EAAEiC,EAAE9B,EAAE,MAAM,GAAG+B,EAAE,EAAEC,EAAE,GAAGE,EAAE,SAAStC,GAAG,IAAIA,EAAEoD,eAAe,CAAC,IAAInD,EAAEmC,EAAE,GAAGjC,EAAEiC,EAAEA,EAAEiB,OAAO,GAAGlB,GAAGnC,EAAE2C,UAAUxC,EAAEwC,UAAU,KAAK3C,EAAE2C,UAAU1C,EAAE0C,UAAU,KAAKR,GAAGnC,EAAEM,MAAM8B,EAAEQ,KAAK5C,KAAKmC,EAAEnC,EAAEM,MAAM8B,EAAE,CAACpC,IAAImC,EAAED,EAAE5B,QAAQ4B,EAAE5B,MAAM6B,EAAED,EAAE1B,QAAQ4B,EAAElC,IAAI,CAAC,EAAEiD,EAAEnC,EAAE,eAAesB,GAAGa,IAAIjD,EAAE+B,EAAE9B,EAAE+B,EAAEjC,GAAGyB,GAAG,WAAWyB,EAAEG,cAAchC,IAAIgB,GAAGpC,GAAE,EAAG,IAAI6B,GAAG,WAAWI,EAAE,EAAEe,GAAG,EAAEhB,EAAE9B,EAAE,MAAM,GAAGF,EAAE+B,EAAE9B,EAAE+B,EAAEjC,EAAE,IAAI,EAAEsD,EAAE,CAACC,SAAQ,EAAGC,SAAQ,GAAIC,EAAE,IAAI/C,KAAKgD,EAAE,SAASxD,EAAEC,GAAGJ,IAAIA,EAAEI,EAAEH,EAAEE,EAAED,EAAE,IAAIS,KAAKiD,EAAE/B,qBAAqBgC,IAAI,EAAEA,EAAE,WAAW,GAAG5D,GAAG,GAAGA,EAAEC,EAAEwD,EAAE,CAAC,IAAItD,EAAE,CAAC0D,UAAU,cAAczD,KAAKL,EAAEwB,KAAKuC,OAAO/D,EAAE+D,OAAOC,WAAWhE,EAAEgE,WAAWrB,UAAU3C,EAAEqC,UAAU4B,gBAAgBjE,EAAEqC,UAAUpC,GAAGE,EAAE+D,SAAS,SAASlE,GAAGA,EAAEI,EAAE,IAAID,EAAE,EAAE,CAAC,EAAEgE,EAAE,SAASnE,GAAG,GAAGA,EAAEgE,WAAW,CAAC,IAAI/D,GAAGD,EAAEqC,UAAU,KAAK,IAAI1B,KAAKmC,YAAYlC,OAAOZ,EAAEqC,UAAU,eAAerC,EAAEwB,KAAK,SAASxB,EAAEC,GAAG,IAAIC,EAAE,WAAWyD,EAAE3D,EAAEC,GAAGG,GAAG,EAAED,EAAE,WAAWC,GAAG,EAAEA,EAAE,WAAWyB,oBAAoB,YAAY3B,EAAEqD,GAAG1B,oBAAoB,gBAAgB1B,EAAEoD,EAAE,EAAEzB,iBAAiB,YAAY5B,EAAEqD,GAAGzB,iBAAiB,gBAAgB3B,EAAEoD,EAAE,CAAhO,CAAkOtD,EAAED,GAAG2D,EAAE1D,EAAED,EAAE,CAAC,EAAE4D,EAAE,SAAS5D,GAAG,CAAC,YAAY,UAAU,aAAa,eAAekE,SAAS,SAASjE,GAAG,OAAOD,EAAEC,EAAEkE,EAAEZ,EAAE,GAAG,EAAEa,EAAE,SAASlE,EAAEgC,GAAG,IAAIC,EAAEC,EAAEE,IAAIG,EAAErC,EAAE,OAAO6C,EAAE,SAASjD,GAAGA,EAAE2C,UAAUP,EAAEI,kBAAkBC,EAAEnC,MAAMN,EAAEiE,gBAAgBjE,EAAE2C,UAAUF,EAAEjC,QAAQoC,KAAK5C,GAAGmC,GAAE,GAAI,EAAEe,EAAElC,EAAE,cAAciC,GAAGd,EAAEF,EAAE/B,EAAEuC,EAAEP,GAAGgB,GAAGxB,GAAG,WAAWwB,EAAEI,cAAchC,IAAI2B,GAAGC,EAAER,YAAY,IAAG,GAAIQ,GAAGnB,GAAG,WAAW,IAAIf,EAAEyB,EAAErC,EAAE,OAAO+B,EAAEF,EAAE/B,EAAEuC,EAAEP,GAAG/B,EAAE,GAAGF,GAAG,EAAED,EAAE,KAAK4D,EAAE9B,kBAAkBd,EAAEiC,EAAE9C,EAAEyC,KAAK5B,GAAG6C,GAAG,GAAG,EAAEQ,EAAE,CAAC,EAAEC,EAAE,SAAStE,EAAEC,GAAG,IAAIC,EAAEC,EAAEmC,IAAIJ,EAAE9B,EAAE,OAAO+B,EAAE,SAASnC,GAAG,IAAIC,EAAED,EAAE2C,UAAU1C,EAAEE,EAAEqC,kBAAkBN,EAAE5B,MAAML,EAAEiC,EAAE1B,QAAQoC,KAAK5C,GAAGE,IAAI,EAAEkC,EAAEpB,EAAE,2BAA2BmB,GAAG,GAAGC,EAAE,CAAClC,EAAE+B,EAAEjC,EAAEkC,EAAEjC,GAAG,IAAIwC,EAAE,WAAW4B,EAAEnC,EAAEzB,MAAM2B,EAAEkB,cAAchC,IAAIa,GAAGC,EAAEM,aAAa2B,EAAEnC,EAAEzB,KAAI,EAAGP,GAAE,GAAI,EAAE,CAAC,UAAU,SAASgE,SAAS,SAASlE,GAAG8B,iBAAiB9B,EAAEyC,EAAE,CAAC8B,MAAK,EAAGd,SAAQ,GAAI,IAAI/B,EAAEe,GAAE,GAAIV,GAAG,SAAS5B,GAAG+B,EAAE9B,EAAE,OAAOF,EAAE+B,EAAEjC,EAAEkC,EAAEjC,GAAG+C,uBAAuB,WAAWA,uBAAuB,WAAWd,EAAE5B,MAAMwC,YAAYlC,MAAMT,EAAEkC,UAAUgC,EAAEnC,EAAEzB,KAAI,EAAGP,GAAE,EAAG,GAAG,GAAG,GAAG,CAAC,EAAEsE,EAAE,SAASxE,GAAG,IAAIC,EAAEC,EAAEE,EAAE,QAAQH,EAAE,WAAW,IAAI,IAAIA,EAAE6C,YAAY2B,iBAAiB,cAAc,IAAI,WAAW,IAAIzE,EAAE8C,YAAY4B,OAAOzE,EAAE,CAAC6D,UAAU,aAAanB,UAAU,GAAG,IAAI,IAAIzC,KAAKF,EAAE,oBAAoBE,GAAG,WAAWA,IAAID,EAAEC,GAAGW,KAAK8D,IAAI3E,EAAEE,GAAGF,EAAE4E,gBAAgB,IAAI,OAAO3E,CAAC,CAAjL,GAAqL,GAAGC,EAAEI,MAAMJ,EAAEK,MAAMN,EAAE4E,cAAc3E,EAAEI,MAAM,GAAGJ,EAAEI,MAAMwC,YAAYlC,MAAM,OAAOV,EAAEM,QAAQ,CAACP,GAAGD,EAAEE,EAAY,CAAT,MAAMF,GAAG,CAAC,EAAE,aAAa2B,SAASmD,WAAWvC,WAAWtC,EAAE,GAAG6B,iBAAiB,QAAQ,WAAW,OAAOS,WAAWtC,EAAE,EAAE,GAAG,C","sources":["../node_modules/web-vitals/dist/web-vitals.js"],"sourcesContent":["var e,t,n,i,r=function(e,t){return{name:e,value:void 0===t?-1:t,delta:0,entries:[],id:\"v2-\".concat(Date.now(),\"-\").concat(Math.floor(8999999999999*Math.random())+1e12)}},a=function(e,t){try{if(PerformanceObserver.supportedEntryTypes.includes(e)){if(\"first-input\"===e&&!(\"PerformanceEventTiming\"in self))return;var n=new PerformanceObserver((function(e){return e.getEntries().map(t)}));return n.observe({type:e,buffered:!0}),n}}catch(e){}},o=function(e,t){var n=function n(i){\"pagehide\"!==i.type&&\"hidden\"!==document.visibilityState||(e(i),t&&(removeEventListener(\"visibilitychange\",n,!0),removeEventListener(\"pagehide\",n,!0)))};addEventListener(\"visibilitychange\",n,!0),addEventListener(\"pagehide\",n,!0)},u=function(e){addEventListener(\"pageshow\",(function(t){t.persisted&&e(t)}),!0)},c=function(e,t,n){var i;return function(r){t.value>=0&&(r||n)&&(t.delta=t.value-(i||0),(t.delta||void 0===i)&&(i=t.value,e(t)))}},f=-1,s=function(){return\"hidden\"===document.visibilityState?0:1/0},m=function(){o((function(e){var t=e.timeStamp;f=t}),!0)},v=function(){return f<0&&(f=s(),m(),u((function(){setTimeout((function(){f=s(),m()}),0)}))),{get firstHiddenTime(){return f}}},d=function(e,t){var n,i=v(),o=r(\"FCP\"),f=function(e){\"first-contentful-paint\"===e.name&&(m&&m.disconnect(),e.startTime-1&&e(t)},f=r(\"CLS\",0),s=0,m=[],v=function(e){if(!e.hadRecentInput){var t=m[0],i=m[m.length-1];s&&e.startTime-i.startTime<1e3&&e.startTime-t.startTime<5e3?(s+=e.value,m.push(e)):(s=e.value,m=[e]),s>f.value&&(f.value=s,f.entries=m,n())}},h=a(\"layout-shift\",v);h&&(n=c(i,f,t),o((function(){h.takeRecords().map(v),n(!0)})),u((function(){s=0,l=-1,f=r(\"CLS\",0),n=c(i,f,t)})))},T={passive:!0,capture:!0},y=new Date,g=function(i,r){e||(e=r,t=i,n=new Date,w(removeEventListener),E())},E=function(){if(t>=0&&t1e12?new Date:performance.now())-e.timeStamp;\"pointerdown\"==e.type?function(e,t){var n=function(){g(e,t),r()},i=function(){r()},r=function(){removeEventListener(\"pointerup\",n,T),removeEventListener(\"pointercancel\",i,T)};addEventListener(\"pointerup\",n,T),addEventListener(\"pointercancel\",i,T)}(t,e):g(t,e)}},w=function(e){[\"mousedown\",\"keydown\",\"touchstart\",\"pointerdown\"].forEach((function(t){return e(t,S,T)}))},L=function(n,f){var s,m=v(),d=r(\"FID\"),p=function(e){e.startTimeperformance.now())return;n.entries=[t],e(n)}catch(e){}},\"complete\"===document.readyState?setTimeout(t,0):addEventListener(\"load\",(function(){return setTimeout(t,0)}))};export{h as getCLS,d as getFCP,L as getFID,F as getLCP,P as getTTFB};\n"],"names":["e","t","n","i","r","name","value","delta","entries","id","concat","Date","now","Math","floor","random","a","PerformanceObserver","supportedEntryTypes","includes","self","getEntries","map","observe","type","buffered","o","document","visibilityState","removeEventListener","addEventListener","u","persisted","c","f","s","m","timeStamp","v","setTimeout","firstHiddenTime","d","disconnect","startTime","push","window","performance","getEntriesByName","requestAnimationFrame","p","l","h","hadRecentInput","length","takeRecords","T","passive","capture","y","g","w","E","entryType","target","cancelable","processingStart","forEach","S","L","b","F","once","P","getEntriesByType","timing","max","navigationStart","responseStart","readyState"],"sourceRoot":""} --------------------------------------------------------------------------------