From c05a4c4cc887b8924426c92860546e25256fc09c Mon Sep 17 00:00:00 2001 From: tozastation Date: Sun, 31 Mar 2024 13:31:03 +0900 Subject: [PATCH] add: googlecloud integration Signed-off-by: tozastation --- README.md | 34 +++++ go.mod | 16 +- go.sum | 17 +++ pkg/ai/prompts.go | 61 +++++++- pkg/integration/googlecloud/gke.go | 125 +++++++++++++++ pkg/integration/googlecloud/gke_test.go | 120 +++++++++++++++ pkg/integration/googlecloud/googlecloud.go | 63 ++++++++ pkg/integration/googlecloud/pubsubclient.go | 160 ++++++++++++++++++++ pkg/integration/integration.go | 8 +- 9 files changed, 589 insertions(+), 15 deletions(-) create mode 100644 pkg/integration/googlecloud/gke.go create mode 100644 pkg/integration/googlecloud/gke_test.go create mode 100644 pkg/integration/googlecloud/googlecloud.go create mode 100644 pkg/integration/googlecloud/pubsubclient.go diff --git a/README.md b/README.md index 5810ffca1c..b58c472ef8 100644 --- a/README.md +++ b/README.md @@ -488,6 +488,40 @@ _See the docs on how to write a custom analyzer_ +
+ GKE Analyzers (integration) + +K8sGPT now supports the ability to integrate with Google Cloud to pull in additional context about your GKE cluster. + +- Feature List + +| analyzer | feature | required parameters | +|----------|-----------------------------|---------------------| +| GKE | ClusterNotificationAnalysis | ✔ | + +- Parameters required for GKE + +```yaml +# Default K8sGPT configuration file path is $HOME/.config/k8sgpt/k8sgpt.yaml +googlecloud: + enable_gke_clusternotificationanalysis: "Whether to enable GKE ClusterNotificationAnalysis(type: bool)" # Default true + gke: + cluster_notification_analysis_list: + - cluster_notification_subscription_id: "YOUR_SUBSCRIPTION_ID(type: string)" + project_id: "YOUR_PROJECT_ID(type: string)" + pubsub: + enable_ack: "Whether to return an Ack when a message is received(type: bool)" # Default true + timeout_sec: "HTTP request timeout value unit is seconds(type: int)" # Default 10 + max_messages: "How many Pub/Sub messages are received in a single request?(type: int)" # Default 1 +``` + +- Activate the GoogleCloud integration +```shell +k8sgpt integrations activate googlecloud +``` + +
+ ## Documentation Find our official documentation available [here](https://docs.k8sgpt.ai) diff --git a/go.mod b/go.mod index aee2d3e9d6..7c971ee0dd 100644 --- a/go.mod +++ b/go.mod @@ -28,6 +28,7 @@ require ( buf.build/gen/go/k8sgpt-ai/k8sgpt/grpc/go v1.3.0-20240213144542-6e830f3fdf19.2 buf.build/gen/go/k8sgpt-ai/k8sgpt/protocolbuffers/go v1.32.0-20240213144542-6e830f3fdf19.1 cloud.google.com/go/storage v1.38.0 + cloud.google.com/go/vertexai v0.7.1 github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1 github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.3.1 github.com/aws/aws-sdk-go v1.50.34 @@ -48,14 +49,15 @@ require ( atomicgo.dev/cursor v0.2.0 // indirect atomicgo.dev/keyboard v0.2.9 // indirect atomicgo.dev/schedule v0.1.0 // indirect - cloud.google.com/go v0.112.0 // indirect + cloud.google.com/go v0.112.1 // indirect cloud.google.com/go/ai v0.3.0 // indirect - cloud.google.com/go/aiplatform v1.59.0 // indirect + cloud.google.com/go/aiplatform v1.60.0 // indirect cloud.google.com/go/compute v1.24.0 // indirect cloud.google.com/go/compute/metadata v0.2.3 // indirect + cloud.google.com/go/container v1.33.0 // indirect cloud.google.com/go/iam v1.1.6 // indirect cloud.google.com/go/longrunning v0.5.5 // indirect - cloud.google.com/go/vertexai v0.7.1 // indirect + cloud.google.com/go/pubsub v1.37.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.2 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.2 // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.2.1 // indirect @@ -76,7 +78,7 @@ require ( github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49 // indirect github.com/google/s2a-go v0.1.7 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect - github.com/googleapis/gax-go/v2 v2.12.1 // indirect + github.com/googleapis/gax-go/v2 v2.12.2 // indirect github.com/gookit/color v1.5.4 // indirect github.com/grafana/regexp v0.0.0-20221122212121-6b5c0a4cb7fd // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect @@ -94,9 +96,9 @@ require ( go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.48.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.48.0 // indirect go.opentelemetry.io/otel/metric v1.23.0 // indirect - google.golang.org/genproto v0.0.0-20240205150955-31a09d347014 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240205150955-31a09d347014 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240213162025-012b6fc9bca9 // indirect + google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240304161311-37d4d3c04a78 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240304161311-37d4d3c04a78 // indirect gopkg.in/evanphx/json-patch.v5 v5.7.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index afce4c50f7..6246633a67 100644 --- a/go.sum +++ b/go.sum @@ -65,6 +65,8 @@ cloud.google.com/go v0.110.10/go.mod h1:v1OoFqYxiBkUrruItNM3eT4lLByNjxmJSV/xDKJN cloud.google.com/go v0.111.0/go.mod h1:0mibmpKP1TyOOFYQY5izo0LnT+ecvOQ0Sg3OdmMiNRU= cloud.google.com/go v0.112.0 h1:tpFCD7hpHFlQ8yPwT3x+QeXqc2T6+n6T+hmABHfDUSM= cloud.google.com/go v0.112.0/go.mod h1:3jEEVwZ/MHU4djK5t5RHuKOA/GbLddgTdVubX1qnPD4= +cloud.google.com/go v0.112.1 h1:uJSeirPke5UNZHIb4SxfZklVSiWWVqW4oXlETwZziwM= +cloud.google.com/go v0.112.1/go.mod h1:+Vbu+Y1UU+I1rjmzeMOb/8RfkKJK2Gyxi1X6jJCZLo4= cloud.google.com/go/accessapproval v1.4.0/go.mod h1:zybIuC3KpDOvotz59lFe5qxRZx6C75OtwbisN56xYB4= cloud.google.com/go/accessapproval v1.5.0/go.mod h1:HFy3tuiGvMdcd/u+Cu5b9NkO1pEICJ46IR82PoUdplw= cloud.google.com/go/accessapproval v1.6.0/go.mod h1:R0EiYnwV5fsRFiKZkPHr6mwyk2wxUJ30nL4j2pcFY2E= @@ -101,6 +103,8 @@ cloud.google.com/go/aiplatform v1.57.0/go.mod h1:pwZMGvqe0JRkI1GWSZCtnAfrR4K1bv6 cloud.google.com/go/aiplatform v1.58.0/go.mod h1:pwZMGvqe0JRkI1GWSZCtnAfrR4K1bv65IHILGA//VEU= cloud.google.com/go/aiplatform v1.59.0 h1:r+P9YStPWrYF52fKyYCQKzTDw4fLiyzLdTEIdxcjmjU= cloud.google.com/go/aiplatform v1.59.0/go.mod h1:eTlGuHOahHprZw3Hio5VKmtThIOak5/qy6pzdsqcQnM= +cloud.google.com/go/aiplatform v1.60.0 h1:0cSrii1ZeLr16MbBoocyy5KVnrSdiQ3KN/vtrTe7RqE= +cloud.google.com/go/aiplatform v1.60.0/go.mod h1:eTlGuHOahHprZw3Hio5VKmtThIOak5/qy6pzdsqcQnM= cloud.google.com/go/analytics v0.11.0/go.mod h1:DjEWCu41bVbYcKyvlws9Er60YE4a//bK6mnhWvQeFNI= cloud.google.com/go/analytics v0.12.0/go.mod h1:gkfj9h6XRf9+TS4bmuhPEShsh3hH8PAZzm/41OOhQd4= cloud.google.com/go/analytics v0.17.0/go.mod h1:WXFa3WSym4IZ+JiKmavYdJwGG/CvpqiqczmL59bTD9M= @@ -370,6 +374,8 @@ cloud.google.com/go/container v1.26.2/go.mod h1:YlO84xCt5xupVbLaMY4s3XNE79MUJ+49 cloud.google.com/go/container v1.27.1/go.mod h1:b1A1gJeTBXVLQ6GGw9/9M4FG94BEGsqJ5+t4d/3N7O4= cloud.google.com/go/container v1.28.0/go.mod h1:b1A1gJeTBXVLQ6GGw9/9M4FG94BEGsqJ5+t4d/3N7O4= cloud.google.com/go/container v1.29.0/go.mod h1:b1A1gJeTBXVLQ6GGw9/9M4FG94BEGsqJ5+t4d/3N7O4= +cloud.google.com/go/container v1.33.0 h1:GS4W16lmqkGP78w7XQ9VEkqayo8CSIXrZkcqbPINvCU= +cloud.google.com/go/container v1.33.0/go.mod h1:u5QBBv/V9dVNK/NtTppCf6T4P8gzp+dQSwx2DqPnAKc= cloud.google.com/go/containeranalysis v0.5.1/go.mod h1:1D92jd8gRR/c0fGMlymRgxWD3Qw9C1ff6/T7mLgVL8I= cloud.google.com/go/containeranalysis v0.6.0/go.mod h1:HEJoiEIu+lEXM+k7+qLCci0h33lX3ZqoYFdmPcoO7s4= cloud.google.com/go/containeranalysis v0.7.0/go.mod h1:9aUL+/vZ55P2CXfuZjS4UjQ9AgXoSw8Ts6lemfmxBxI= @@ -889,6 +895,8 @@ cloud.google.com/go/pubsub v1.28.0/go.mod h1:vuXFpwaVoIPQMGXqRyUQigu/AX1S3IWugR9 cloud.google.com/go/pubsub v1.30.0/go.mod h1:qWi1OPS0B+b5L+Sg6Gmc9zD1Y+HaM0MdUr7LsupY1P4= cloud.google.com/go/pubsub v1.32.0/go.mod h1:f+w71I33OMyxf9VpMVcZbnG5KSUkCOUHYpFd5U1GdRc= cloud.google.com/go/pubsub v1.33.0/go.mod h1:f+w71I33OMyxf9VpMVcZbnG5KSUkCOUHYpFd5U1GdRc= +cloud.google.com/go/pubsub v1.37.0 h1:0uEEfaB1VIJzabPpwpZf44zWAKAme3zwKKxHk7vJQxQ= +cloud.google.com/go/pubsub v1.37.0/go.mod h1:YQOQr1uiUM092EXwKs56OPT650nwnawc+8/IjoUeGzQ= cloud.google.com/go/pubsublite v1.5.0/go.mod h1:xapqNQ1CuLfGi23Yda/9l4bBCKz/wC3KIJ5gKcxveZg= cloud.google.com/go/pubsublite v1.6.0/go.mod h1:1eFCS0U11xlOuMFV/0iBqw3zP12kddMeCbj/F3FSj9k= cloud.google.com/go/pubsublite v1.7.0/go.mod h1:8hVMwRXfDfvGm3fahVbtDbiLePT3gpoiJYJY+vxWxVM= @@ -1697,6 +1705,8 @@ github.com/googleapis/gax-go/v2 v2.11.0/go.mod h1:DxmR61SGKkGLa2xigwuZIQpkCI2S5i github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU= github.com/googleapis/gax-go/v2 v2.12.1 h1:9F8GV9r9ztXyAi00gsMQHNoF51xPZm8uj1dpYt2ZETM= github.com/googleapis/gax-go/v2 v2.12.1/go.mod h1:61M8vcyyXR2kqKFxKrfA22jaA8JGF7Dc8App1U3H6jc= +github.com/googleapis/gax-go/v2 v2.12.2 h1:mhN09QQW1jEWeMF74zGR81R30z4VJzjZsfkUhuHF+DA= +github.com/googleapis/gax-go/v2 v2.12.2/go.mod h1:61M8vcyyXR2kqKFxKrfA22jaA8JGF7Dc8App1U3H6jc= github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ= @@ -2150,6 +2160,7 @@ go.opentelemetry.io/otel/metric v1.23.0/go.mod h1:MqUW2X2a6Q8RN96E2/nqNoT+z9BSms go.opentelemetry.io/otel/sdk v1.19.0/go.mod h1:NedEbbS4w3C6zElbLdPJKOpJQOrGUJ+GfzpjUvI0v1A= go.opentelemetry.io/otel/sdk v1.21.0 h1:FTt8qirL1EysG6sTQRZ5TokkU8d0ugCj8htOgThZXQ8= go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E= +go.opentelemetry.io/otel/sdk v1.22.0 h1:6coWHw9xw7EfClIC/+O31R8IY3/+EiRFHevmHafB2Gw= go.opentelemetry.io/otel/trace v1.19.0/go.mod h1:mfaSyvGyEJEI0nyV2I4qhNQnbBOUUmYZpYojqMnX2vo= go.opentelemetry.io/otel/trace v1.23.0 h1:37Ik5Ib7xfYVb4V1UtnT97T1jI+AoIYkJyPkuL4iJgI= go.opentelemetry.io/otel/trace v1.23.0/go.mod h1:GSGTbIClEsuZrGIzoEHqsVfxgn5UkggkflQwDScNUsk= @@ -2857,6 +2868,8 @@ google.golang.org/genproto v0.0.0-20240102182953-50ed04b92917/go.mod h1:pZqR+glS google.golang.org/genproto v0.0.0-20240116215550-a9fa1716bcac/go.mod h1:+Rvu7ElI+aLzyDQhpHMFMMltsD6m7nqpuWDd2CwJw3k= google.golang.org/genproto v0.0.0-20240205150955-31a09d347014 h1:g/4bk7P6TPMkAUbUhquq98xey1slwvuVJPosdBqYJlU= google.golang.org/genproto v0.0.0-20240205150955-31a09d347014/go.mod h1:xEgQu1e4stdSSsxPDK8Azkrk/ECl5HvdPf6nbZrTS5M= +google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 h1:9+tzLLstTlPTRyJTh+ah5wIMsBW5c4tQwGTN3thOW9Y= +google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9/go.mod h1:mqHbVIp48Muh7Ywss/AD6I5kNVKZMmAa/QEW58Gxp2s= google.golang.org/genproto/googleapis/api v0.0.0-20230525234020-1aefcd67740a/go.mod h1:ts19tUU+Z0ZShN1y3aPyq2+O3d5FUNNgT6FtOzmrNn8= google.golang.org/genproto/googleapis/api v0.0.0-20230525234035-dd9d682886f9/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= google.golang.org/genproto/googleapis/api v0.0.0-20230526203410-71b5a4ffd15e/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= @@ -2881,6 +2894,8 @@ google.golang.org/genproto/googleapis/api v0.0.0-20240102182953-50ed04b92917/go. google.golang.org/genproto/googleapis/api v0.0.0-20240125205218-1f4bbc51befe/go.mod h1:4jWUdICTdgc3Ibxmr8nAJiiLHwQBY0UI0XZcEMaFKaA= google.golang.org/genproto/googleapis/api v0.0.0-20240205150955-31a09d347014 h1:x9PwdEgd11LgK+orcck69WVRo7DezSO4VUMPI4xpc8A= google.golang.org/genproto/googleapis/api v0.0.0-20240205150955-31a09d347014/go.mod h1:rbHMSEDyoYX62nRVLOCc4Qt1HbsdytAYoVwgjiOhF3I= +google.golang.org/genproto/googleapis/api v0.0.0-20240304161311-37d4d3c04a78 h1:SzXBGiWM1LNVYLCRP3e0/Gsze804l4jGoJ5lYysEO5I= +google.golang.org/genproto/googleapis/api v0.0.0-20240304161311-37d4d3c04a78/go.mod h1:O1cOfN1Cy6QEYr7VxtjOyP5AdAuR0aJ/MYZaaof623Y= google.golang.org/genproto/googleapis/bytestream v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:ylj+BE99M198VPbBh6A8d9n3w8fChvyLK3wwBOjXBFA= google.golang.org/genproto/googleapis/bytestream v0.0.0-20230807174057-1744710a1577/go.mod h1:NjCQG/D8JandXxM57PZbAJL1DCNL6EypA0vPPwfsc7c= google.golang.org/genproto/googleapis/bytestream v0.0.0-20231030173426-d783a09b4405/go.mod h1:GRUCuLdzVqZte8+Dl/D4N25yLzcGqqWaYkeVOwulFqw= @@ -2909,6 +2924,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20240116215550-a9fa1716bcac/go. google.golang.org/genproto/googleapis/rpc v0.0.0-20240125205218-1f4bbc51befe/go.mod h1:PAREbraiVEVGVdTZsVWjSbbTtSyGbAgIIvni8a8CD5s= google.golang.org/genproto/googleapis/rpc v0.0.0-20240213162025-012b6fc9bca9 h1:hZB7eLIaYlW9qXRfCq/qDaPdbeY3757uARz5Vvfv+cY= google.golang.org/genproto/googleapis/rpc v0.0.0-20240213162025-012b6fc9bca9/go.mod h1:YUWgXUFRPfoYK1IHMuxH5K6nPEXSCzIMljnQ59lLRCk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240304161311-37d4d3c04a78 h1:Xs9lu+tLXxLIfuci70nG4cpwaRC+mRQPUL7LoIeDJC4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240304161311-37d4d3c04a78/go.mod h1:UCOku4NytXMJuLQE5VuqA5lX3PcHCBo8pxNyvkf4xBs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= diff --git a/pkg/ai/prompts.go b/pkg/ai/prompts.go index e0ce72cf8d..39330ceef1 100644 --- a/pkg/ai/prompts.go +++ b/pkg/ai/prompts.go @@ -48,12 +48,63 @@ const ( - Containers: - {list of container names} ` + gke_cluster_notification_upgrade_prompt = ` +Return your prompt in this language: %s. +This is a UpgradeEvent or UpgradeAvailabilityEvent of Google Kubernetes Engine (GKE) cluster notification. +Provide the output according to the message format. +--- +%s +--- +Return the message format: +**Notification** +{The payload within the first triple dash surrounded by code blocks} +**Notification Attribute** +- {project id} +- {cluster location} +- {cluster name} +**Explanation** +{severity} +**{Next Action}** +{what should I do next} +**Reference URL** +{reference URL} +` + gke_cluster_notification_security_bulletin_event_prompt = ` +Return your prompt in this language: %s. +This is a SecurityBulletinEvent of Google Kubernetes Engine (GKE) cluster notification. +Explain the following that and the detail risk or root cause of the CVE ID, then provide a solution. +--- +%s +--- +Return the message format: +**Notification** +{The payload within the first triple dash surrounded by code blocks} +**Notification Attribute** +- {project id} +- {cluster location} +- {cluster name} +**Severity** +- {severity} +**CVE ID** +- {CVE ID} +**Description** +- {description} +- {danger of this vulnerability in kubernetes cluster} +**Solution** +- {solution} +**Reference URL** +- {reference URL} +) +` ) var PromptMap = map[string]string{ - "default": default_prompt, - "VulnerabilityReport": trivy_vuln_prompt, // for Trivy integration, the key should match `Result.Kind` in pkg/common/types.go - "ConfigAuditReport": trivy_conf_prompt, - "PrometheusConfigValidate": prom_conf_prompt, - "PrometheusConfigRelabelReport": prom_relabel_prompt, + "default": default_prompt, + "VulnerabilityReport": trivy_vuln_prompt, // for Trivy integration, the key should match `Result.Kind` in pkg/common/types.go + "ConfigAuditReport": trivy_conf_prompt, + "PrometheusConfigValidate": prom_conf_prompt, + "PrometheusConfigRelabelReport": prom_relabel_prompt, + "GKEClusterNotificationUpgradeEvent": gke_cluster_notification_upgrade_prompt, + "GKEClusterNotificationUpgradeAvailabilityEvent": gke_cluster_notification_upgrade_prompt, + "GKEClusterNotificationSecurityBulletinEvent": gke_cluster_notification_security_bulletin_event_prompt, } diff --git a/pkg/integration/googlecloud/gke.go b/pkg/integration/googlecloud/gke.go new file mode 100644 index 0000000000..8c62aeea41 --- /dev/null +++ b/pkg/integration/googlecloud/gke.go @@ -0,0 +1,125 @@ +package googlecloud + +import ( + "bytes" + "encoding/json" + "fmt" + "github.com/k8sgpt-ai/k8sgpt/pkg/common" + "github.com/spf13/viper" +) + +const ( + securityBulletinEventURL = "type.googleapis.com/google.container.v1beta1.SecurityBulletinEvent" + upgradeAvailableEventURL = "type.googleapis.com/google.container.v1beta1.UpgradeAvailableEvent" + upgradeEventURL = "type.googleapis.com/google.container.v1beta1.UpgradeEvent" +) + +type GKEAnalyzer struct { + EnableClusterNotificationAnalysis bool `mapstructure:"enable_cluster_notification_analysis"` +} + +type GKEClusterNotificationAnalysis struct { + ProjectID string `mapstructure:"project_id"` + ClusterNotificationSubscriptionID string `mapstructure:"cluster_notification_subscription_id"` +} + +type clusterNotification struct { + ProjectID string `json:"project_id"` + ClusterLocation string `json:"cluster_location"` + ClusterName string `json:"cluster_name"` + TypeURL string `json:"type_url"` + Payload map[string]interface{} `json:"payload"` +} + +func (g *GKEAnalyzer) Analyze(analysis common.Analyzer) ([]common.Result, error) { + var cr []common.Result + if g.EnableClusterNotificationAnalysis { + var gkeClusterNotificationAnalysisList []GKEClusterNotificationAnalysis + if err := viper.UnmarshalKey("googlecloud.gke.cluster_notification_analysis_list", &gkeClusterNotificationAnalysisList); err != nil { + return nil, err + } + for _, gkeClusterNotificationAnalysis := range gkeClusterNotificationAnalysisList { + if gkeClusterNotificationAnalysis.ProjectID == "" || gkeClusterNotificationAnalysis.ClusterNotificationSubscriptionID == "" { + continue + } + pubsubClient, err := newPubSubClient(analysis.Context, gkeClusterNotificationAnalysis.ProjectID, gkeClusterNotificationAnalysis.ClusterNotificationSubscriptionID) + if err != nil { + return nil, err + } + pullSubscriptionResp, err := pubsubClient.PullSubscription(analysis.Context) + if err != nil { + return nil, err + } + if pullSubscriptionResp == nil { + continue + } + for _, msg := range pullSubscriptionResp.ReceivedMessages { + result, err := g.analyzeClusterNotification(&msg) + if err != nil { + return nil, err + } + cr = append(cr, result...) + } + } + } + analysis.Results = append(analysis.Results, cr...) + return cr, nil +} + +func (g *GKEAnalyzer) analyzeClusterNotification(msg *PubSubReceivedMessage) ([]common.Result, error) { + var cr []common.Result + // Unmarshal the PubSubReceivedMessage (ClusterNotification) + var payload map[string]interface{} + err := json.Unmarshal([]byte(msg.Message.Attributes["payload"]), &payload) + if err != nil { + return nil, err + } + clusterNotification := clusterNotification{ + ProjectID: msg.Message.Attributes["project_id"], + ClusterLocation: msg.Message.Attributes["cluster_location"], + ClusterName: msg.Message.Attributes["cluster_name"], + TypeURL: msg.Message.Attributes["type_url"], + Payload: payload, + } + // Indent the payload for better readability + var buf bytes.Buffer + if err := json.Indent(&buf, []byte(msg.Message.Attributes["payload"]), "", " "); err != nil { + return nil, err + } + switch clusterNotification.TypeURL { + case securityBulletinEventURL: + cr = append(cr, common.Result{ + Kind: "GKEClusterNotificationSecurityBulletinEvent", + Name: "SecurityBulletinEvent", + Error: []common.Failure{ + {Text: fmt.Sprintf( + "project_id: %s\ncluster_location: %s\ncluster_name: %s\npayload: %s\n", + clusterNotification.ProjectID, clusterNotification.ClusterLocation, clusterNotification.ClusterName, buf.String(), + ), KubernetesDoc: "", Sensitive: nil}, + }, + }) + case upgradeAvailableEventURL: + cr = append(cr, common.Result{ + Kind: "GKEClusterNotificationUpgradeAvailabilityEvent", + Name: "UpgradeAvailableEvent", + Error: []common.Failure{ + {Text: fmt.Sprintf( + "project_id: %s\ncluster_location: %s\ncluster_name: %s\npayload: %s\n", + clusterNotification.ProjectID, clusterNotification.ClusterLocation, clusterNotification.ClusterName, buf.String()), + KubernetesDoc: "", Sensitive: nil}, + }, + }) + case upgradeEventURL: + cr = append(cr, common.Result{ + Kind: "GKEClusterNotificationUpgradeEvent", + Name: "UpgradeEvent", + Error: []common.Failure{ + {Text: fmt.Sprintf( + "project_id: %s\ncluster_location: %s\ncluster_name: %s\npayload: %s\n", + clusterNotification.ProjectID, clusterNotification.ClusterLocation, clusterNotification.ClusterName, buf.String()), + KubernetesDoc: "", Sensitive: nil}, + }, + }) + } + return cr, nil +} diff --git a/pkg/integration/googlecloud/gke_test.go b/pkg/integration/googlecloud/gke_test.go new file mode 100644 index 0000000000..902d08a594 --- /dev/null +++ b/pkg/integration/googlecloud/gke_test.go @@ -0,0 +1,120 @@ +package googlecloud + +import ( + "fmt" + "testing" + "time" +) + +func Test_GKEAnalyzer_analyzeClusterNotification(t *testing.T) { + type args struct { + msg PubSubReceivedMessage + } + tests := []struct { + name string + args args + want int + wantErr bool + }{ + { + name: "Test_GKEAnalyzer_analyzeClusterNotification_SecurityBulletinEvent", + args: args{ + msg: PubSubReceivedMessage{ + Message: struct { + Data []byte `json:"data"` + Attributes map[string]string `json:"attributes"` + MessageID string `json:"messageId"` + PublishTime time.Time `json:"publishTime"` + OrderingKey string `json:"orderingKey"` + }(struct { + Data []byte + Attributes map[string]string + MessageID string + PublishTime time.Time + OrderingKey string + }{ + Attributes: map[string]string{ + "project_id": "123456789", + "cluster_location": "us-central1-c", + "cluster_name": "example-cluster", + "type_url": "type.googleapis.com/google.container.v1beta1.SecurityBulletinEvent", + "payload": "{\"resourceTypeAffected\":\"RESOURCE_TYPE_CONTROLPLANE\",\"bulletinId\":\"GCP-2021-001\",\"cveIds\":[\"CVE-2021-3156\"],\"severity\":\"Medium\",\"briefDescription\":\"A vulnerability was recently discovered in the Linux utility sudo, described in CVE-2021-3156, that may allow an attacker with unprivileged local shell access on a system with sudo installed to escalate their privileges to root on the system.\",\"affectedSupportedMinors\":[\"1.18\",\"1.19\"],\"patchedVersions\":[\"1.18.9-gke.1900\",\"1.19.9-gke.1900\"],\"suggestedUpgradeTarget\":\"1.19.9-gke.1900\",\"bulletinUri\":\"https://cloud.google.com/anthos/clusters/docs/security-bulletins#gcp-2021-001\"}", + }, + })}, + }, + want: 1, + }, + { + name: "Test_GKEAnalyzer_analyzeClusterNotification_UpgradeAvailableEvent", + args: args{ + msg: PubSubReceivedMessage{Message: struct { + Data []byte `json:"data"` + Attributes map[string]string `json:"attributes"` + MessageID string `json:"messageId"` + PublishTime time.Time `json:"publishTime"` + OrderingKey string `json:"orderingKey"` + }(struct { + Data []byte + Attributes map[string]string + MessageID string + PublishTime time.Time + OrderingKey string + }{ + Attributes: map[string]string{ + "project_id": "123456789", + "cluster_location": "us-central1-c", + "cluster_name": "example-cluster", + "type_url": "type.googleapis.com/google.container.v1beta1.UpgradeAvailableEvent", + "payload": "{ \"version\":\"1.17.15-gke.800\",\n \"resourceType\":\"MASTER\",\n \"releaseChannel\":{\"channel\":\"RAPID\"},\n \"windowsVersions\": [\n {\n \"imageType\": \"WINDOWS_SAC\",\n \"osVersion\": \"10.0.18363.1198\",\n \"supportEndDate\": {\n \"day\": 10,\n \"month\": 5,\n \"year\": 2022\n }\n },\n {\n \"imageType\": \"WINDOWS_LTSC\",\n \"osVersion\": \"10.0.17763.1577\",\n \"supportEndDate\": {\n \"day\": 9,\n \"month\": 1,\n \"year\": 2024\n }\n }\n ]\n}", + }, + })}, + }, + want: 1, + }, + { + name: "Test_GKEAnalyzer_analyzeClusterNotification_UpgradeEvent", + args: args{ + msg: PubSubReceivedMessage{Message: struct { + Data []byte `json:"data"` + Attributes map[string]string `json:"attributes"` + MessageID string `json:"messageId"` + PublishTime time.Time `json:"publishTime"` + OrderingKey string `json:"orderingKey"` + }(struct { + Data []byte + Attributes map[string]string + MessageID string + PublishTime time.Time + OrderingKey string + }{ + Attributes: map[string]string{ + "project_id": "123456789", + "cluster_location": "us-central1-c", + "cluster_name": "example-cluster", + "type_url": "type.googleapis.com/google.container.v1beta1.UpgradeEvent", + "payload": "{\"resourceType\":\"MASTER\",\"operation\":\"operation-1595889094437-87b7254a\",\"operationStartTime\":\"2020-07-27T22:31:34.437652293Z\",\"currentVersion\":\"1.15.12-gke.2\",\"targetVersion\":\"1.15.12-gke.9\"}", + }, + })}, + }, + want: 1, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := &GKEAnalyzer{ + EnableClusterNotificationAnalysis: true, + } + got, err := g.analyzeClusterNotification(&tt.args.msg) + if (err != nil) != tt.wantErr { + t.Errorf("analyzeClusterNotification() error = %v, wantErr %v", err, tt.wantErr) + return + } + for _, result := range got { + fmt.Println(result) + } + if len(got) != tt.want { + t.Errorf("analyzeClusterNotification() got = %v, want %v", len(got), tt.want) + } + }) + } +} diff --git a/pkg/integration/googlecloud/googlecloud.go b/pkg/integration/googlecloud/googlecloud.go new file mode 100644 index 0000000000..d9394ec667 --- /dev/null +++ b/pkg/integration/googlecloud/googlecloud.go @@ -0,0 +1,63 @@ +package googlecloud + +import ( + "github.com/k8sgpt-ai/k8sgpt/pkg/common" + "github.com/spf13/viper" +) + +type GoogleCloud struct{} + +func (g *GoogleCloud) Deploy(namespace string) error { + return nil +} + +func (g *GoogleCloud) UnDeploy(namespace string) error { + return nil +} + +func (g *GoogleCloud) GetAnalyzerName() []string { + return []string{ + "GKEClusterNotificationAnalysis", + } +} + +func (g *GoogleCloud) GetNamespace() (string, error) { + return "", nil +} + +func (g *GoogleCloud) OwnsAnalyzer(s string) bool { + for _, az := range g.GetAnalyzerName() { + if s == az { + return true + } + } + return false +} + +func (g *GoogleCloud) IsActivate() bool { + activeFilters := viper.GetStringSlice("active_filters") + + for _, filter := range g.GetAnalyzerName() { + for _, af := range activeFilters { + if af == filter { + return true + } + } + } + + return false +} + +func NewGoogleCloud() *GoogleCloud { + return &GoogleCloud{} +} + +func (g *GoogleCloud) AddAnalyzer(mergedMap *map[string]common.IAnalyzer) { + enableClusterNotificationAnalysis := true + if viper.Get("googlecloud.enable_gke_clusternotificationanalysis") != nil { + enableClusterNotificationAnalysis = viper.GetBool("googlecloud.enable_gke_clusternotificationanalysis") + } + (*mergedMap)["GKEClusterNotificationAnalysis"] = &GKEAnalyzer{ + EnableClusterNotificationAnalysis: enableClusterNotificationAnalysis, + } +} diff --git a/pkg/integration/googlecloud/pubsubclient.go b/pkg/integration/googlecloud/pubsubclient.go new file mode 100644 index 0000000000..7c1540a373 --- /dev/null +++ b/pkg/integration/googlecloud/pubsubclient.go @@ -0,0 +1,160 @@ +package googlecloud + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "github.com/spf13/viper" + "golang.org/x/oauth2/google" + "net/http" + "os" + "time" +) + +/* +PubSubClient is a client for Google Cloud Pub/Sub. +PubSub Client is used to receive GKE cluster notifications from Cloud Pub/Sub; +the reason for using the REST endpoint instead of gRPC is that we wanted to query only as much as needed, without using gRPC Streaming. +*/ + +type PubSubClient struct { + ProjectID string + SubscriptionID string + HTTPClient *http.Client + TimeoutSec int `json:"timeout_sec"` + EnableAck bool `json:"enable_ack"` + MaxMessages int `json:"max_messages"` +} + +func newPubSubClient(ctx context.Context, projectID, subscriptionID string) (*PubSubClient, error) { + googleDefaultClient, err := google.DefaultClient(ctx, []string{ + "https://www.googleapis.com/auth/pubsub", + "https://www.googleapis.com/auth/cloud-platform"}..., + ) + if err != nil { + return nil, err + } + timeoutSec := 10 + if viper.Get("googlecloud.pubsub.timeout_sec") != nil { + timeoutSec = viper.GetInt("googlecloud.pubsub.timeout_sec") + } + enableAcK := true + if viper.Get("googlecloud.pubsub.enable_ack") != nil { + enableAcK = viper.GetBool("googlecloud.pubsub.enable_ack") + } + maxMessage := 1 + if viper.Get("googlecloud.pubsub.max_messages") != nil { + maxMessage = viper.GetInt("googlecloud.pubsub.max_messages") + } + return &PubSubClient{ + ProjectID: projectID, + SubscriptionID: subscriptionID, + HTTPClient: googleDefaultClient, + TimeoutSec: timeoutSec, + EnableAck: enableAcK, + MaxMessages: maxMessage, + }, nil +} + +type PullSubscriptionRequest struct { + ReturnImmediately bool `json:"returnImmediately"` + MaxMessages int `json:"maxMessages"` +} + +type PullSubscriptionResponse struct { + ReceivedMessages []PubSubReceivedMessage `json:"receivedMessages"` +} + +type PubSubReceivedMessage struct { + AckID string `json:"ackId"` + Message struct { + Data []byte `json:"data"` + Attributes map[string]string `json:"attributes"` + MessageID string `json:"messageId"` + PublishTime time.Time `json:"publishTime"` + OrderingKey string `json:"orderingKey"` + } `json:"message"` + DeliverAttempt int `json:"deliveryAttempt"` +} + +func (p *PubSubClient) GetPullSubscriptionEndpoint() string { + // PullSubscription Endpoint Example: https://pubsub.googleapis.com/v1/projects/myproject/subscriptions/mysubscription:pull + return fmt.Sprintf("https://pubsub.googleapis.com/v1/projects/%s/subscriptions/%s:pull", p.ProjectID, p.SubscriptionID) +} + +func (p *PubSubClient) PullSubscription(ctx context.Context) (*PullSubscriptionResponse, error) { + reqBody := PullSubscriptionRequest{ + MaxMessages: p.MaxMessages, + } + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(reqBody); err != nil { + return nil, err + } + ctx, cancel := context.WithTimeout(ctx, time.Duration(p.TimeoutSec)*time.Second) + defer cancel() + req, err := http.NewRequestWithContext(ctx, http.MethodPost, p.GetPullSubscriptionEndpoint(), &buf) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/json") + resp, err := p.HTTPClient.Do(req) + if err != nil { + if os.IsTimeout(err) { + fmt.Println("error by timeout. maybe message is not found.") + return nil, nil + } + return nil, err + } + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("pull subscription failed: %s", resp.Status) + } + defer resp.Body.Close() + var pullSubscriptionResponse PullSubscriptionResponse + if err := json.NewDecoder(resp.Body).Decode(&pullSubscriptionResponse); err != nil { + return nil, err + } + if p.EnableAck { + ackIDs := make([]string, 0, len(pullSubscriptionResponse.ReceivedMessages)) + for _, msg := range pullSubscriptionResponse.ReceivedMessages { + ackIDs = append(ackIDs, msg.AckID) + } + if err := p.AckSubscription(ctx, ackIDs); err != nil { + return nil, err + } + } + return &pullSubscriptionResponse, nil +} + +type AckSubscriptionRequest struct { + AckIDs []string `json:"ackIds"` +} + +func (p *PubSubClient) GetAckSubscriptionEndpoint() string { + // AckSubscription Endpoint Example: https://pubsub.googleapis.com/v1/projects/myproject/subscriptions/mysubscription:acknowledge + return fmt.Sprintf("https://pubsub.googleapis.com/v1/projects/%s/subscriptions/%s:acknowledge", p.ProjectID, p.SubscriptionID) +} + +func (p *PubSubClient) AckSubscription(ctx context.Context, ackIDs []string) error { + reqBody := AckSubscriptionRequest{ + AckIDs: ackIDs, + } + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(reqBody); err != nil { + return err + } + req, err := http.NewRequestWithContext(ctx, http.MethodPost, p.GetAckSubscriptionEndpoint(), &buf) + if err != nil { + return err + } + req.Header.Set("Content-Type", "application/json") + resp, err := p.HTTPClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("acknowledge subscription failed: %s", resp.Status) + } + return nil +} diff --git a/pkg/integration/integration.go b/pkg/integration/integration.go index 2318427634..2e15e4779a 100644 --- a/pkg/integration/integration.go +++ b/pkg/integration/integration.go @@ -17,6 +17,7 @@ import ( "errors" "fmt" "github.com/k8sgpt-ai/k8sgpt/pkg/integration/aws" + "github.com/k8sgpt-ai/k8sgpt/pkg/integration/googlecloud" "github.com/k8sgpt-ai/k8sgpt/pkg/common" "github.com/k8sgpt-ai/k8sgpt/pkg/integration/prometheus" @@ -46,9 +47,10 @@ type Integration struct { } var integrations = map[string]IIntegration{ - "trivy": trivy.NewTrivy(), - "prometheus": prometheus.NewPrometheus(), - "aws": aws.NewAWS(), + "trivy": trivy.NewTrivy(), + "prometheus": prometheus.NewPrometheus(), + "aws": aws.NewAWS(), + "googlecloud": googlecloud.NewGoogleCloud(), } func NewIntegration() *Integration {