From f58f02560ffeede80f1051e9a97c1ef07c5ad500 Mon Sep 17 00:00:00 2001 From: Ash <159829404+hedge-sparrow@users.noreply.github.com> Date: Thu, 3 Oct 2024 13:18:36 +0100 Subject: [PATCH 01/25] Allow goldpinger / goldpinger util images to be set in collector spec (#1635) * Add image parameter to the goldpinger collector * Pass image directly as a function arg Also allow util image to be set in spec * Remove pointless util image override * Update pkg/collect/goldpinger.go Co-authored-by: Evans Mungai * Simplify image override --------- Co-authored-by: Evans Mungai --- config/crds/troubleshoot.sh_collectors.yaml | 2 ++ config/crds/troubleshoot.sh_preflights.yaml | 2 ++ pkg/apis/troubleshoot/v1beta2/collector_shared.go | 1 + pkg/collect/goldpinger.go | 9 ++++++++- schemas/collector-troubleshoot-v1beta2.json | 3 +++ schemas/preflight-troubleshoot-v1beta2.json | 3 +++ 6 files changed, 19 insertions(+), 1 deletion(-) diff --git a/config/crds/troubleshoot.sh_collectors.yaml b/config/crds/troubleshoot.sh_collectors.yaml index 02a85fa29..41665245c 100644 --- a/config/crds/troubleshoot.sh_collectors.yaml +++ b/config/crds/troubleshoot.sh_collectors.yaml @@ -354,6 +354,8 @@ spec: type: string exclude: type: BoolString + image: + type: string namespace: type: string podLaunchOptions: diff --git a/config/crds/troubleshoot.sh_preflights.yaml b/config/crds/troubleshoot.sh_preflights.yaml index 38e900de7..d762ce2c6 100644 --- a/config/crds/troubleshoot.sh_preflights.yaml +++ b/config/crds/troubleshoot.sh_preflights.yaml @@ -2083,6 +2083,8 @@ spec: type: string exclude: type: BoolString + image: + type: string namespace: type: string podLaunchOptions: diff --git a/pkg/apis/troubleshoot/v1beta2/collector_shared.go b/pkg/apis/troubleshoot/v1beta2/collector_shared.go index 04181d7ac..1cc212f52 100644 --- a/pkg/apis/troubleshoot/v1beta2/collector_shared.go +++ b/pkg/apis/troubleshoot/v1beta2/collector_shared.go @@ -279,6 +279,7 @@ type Helm struct { type Goldpinger struct { CollectorMeta `json:",inline" yaml:",inline"` Namespace string `json:"namespace,omitempty" yaml:"namespace,omitempty"` + Image string `json:"image,omitempty" yaml:"image,omitempty"` ServiceAccountName string `json:"serviceAccountName,omitempty" yaml:"serviceAccountName,omitempty"` CollectDelay string `json:"collectDelay,omitempty" yaml:"collectDelay,omitempty"` PodLaunchOptions *PodLaunchOptions `json:"podLaunchOptions,omitempty" yaml:"podLaunchOptions,omitempty"` diff --git a/pkg/collect/goldpinger.go b/pkg/collect/goldpinger.go index c7142b949..b7a7eb801 100644 --- a/pkg/collect/goldpinger.go +++ b/pkg/collect/goldpinger.go @@ -26,6 +26,8 @@ import ( "k8s.io/utils/ptr" ) +var goldpingerImage = "bloomberg/goldpinger:3.10.1" + // Collect goldpinger results from goldpinger service running in a cluster // The results are stored in goldpinger/check_all.json since we use // the /check_all endpoint @@ -299,6 +301,10 @@ func (c *CollectGoldpinger) createGoldpingerRoleBinding(ns string) (*rbacv1.Role func (c *CollectGoldpinger) createGoldpingerDaemonSet(ns, svcAccName string) (*appsv1.DaemonSet, error) { ds := &appsv1.DaemonSet{} + if c.Collector.Image != "" { + goldpingerImage = c.Collector.Image + } + ds.ObjectMeta = metav1.ObjectMeta{ Name: "ts-goldpinger", Namespace: ns, @@ -320,7 +326,7 @@ func (c *CollectGoldpinger) createGoldpingerDaemonSet(ns, svcAccName string) (*a Containers: []corev1.Container{ { Name: "goldpinger-daemon", - Image: "bloomberg/goldpinger:3.10.1", + Image: goldpingerImage, ImagePullPolicy: corev1.PullIfNotPresent, Env: []corev1.EnvVar{ { @@ -479,6 +485,7 @@ func (c *CollectGoldpinger) runPodAndCollectGPResults(url string, progressChan c namespace := "default" serviceAccountName := "" image := constants.GP_DEFAULT_IMAGE + var imagePullSecret *troubleshootv1beta2.ImagePullSecrets if c.Collector.PodLaunchOptions != nil { diff --git a/schemas/collector-troubleshoot-v1beta2.json b/schemas/collector-troubleshoot-v1beta2.json index 21241d7fd..a539ae297 100644 --- a/schemas/collector-troubleshoot-v1beta2.json +++ b/schemas/collector-troubleshoot-v1beta2.json @@ -484,6 +484,9 @@ "exclude": { "oneOf": [{"type": "string"},{"type": "boolean"}] }, + "image": { + "type": "string" + }, "namespace": { "type": "string" }, diff --git a/schemas/preflight-troubleshoot-v1beta2.json b/schemas/preflight-troubleshoot-v1beta2.json index 0555adce9..7b4ddd0f7 100644 --- a/schemas/preflight-troubleshoot-v1beta2.json +++ b/schemas/preflight-troubleshoot-v1beta2.json @@ -3148,6 +3148,9 @@ "exclude": { "oneOf": [{"type": "string"},{"type": "boolean"}] }, + "image": { + "type": "string" + }, "namespace": { "type": "string" }, From 0240a632c91ee94acc4ba47c6ce644c2c5cd1e64 Mon Sep 17 00:00:00 2001 From: Evans Mungai Date: Thu, 3 Oct 2024 08:55:53 -0500 Subject: [PATCH 02/25] chore: Collect endpointslices resources (#1636) Signed-off-by: Evans Mungai --- pkg/collect/cluster_resources.go | 43 ++++++++++++++++++++++++++++++++ pkg/constants/constants.go | 1 + 2 files changed, 44 insertions(+) diff --git a/pkg/collect/cluster_resources.go b/pkg/collect/cluster_resources.go index 999c12f5e..c8e447083 100644 --- a/pkg/collect/cluster_resources.go +++ b/pkg/collect/cluster_resources.go @@ -367,6 +367,13 @@ func (c *CollectClusterResources) Collect(progressChan chan<- interface{}) (Coll } output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s-errors.json", constants.CLUSTER_RESOURCES_ENDPOINTS)), marshalErrors(endpointsErrors)) + // endpointslices + endpointslices, endpointslicesErrors := endpointslices(ctx, client, namespaceNames) + for k, v := range endpointslices { + _ = output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_ENDPOINTSICES, k), bytes.NewBuffer(v)) + } + _ = output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s-errors.json", constants.CLUSTER_RESOURCES_ENDPOINTSICES)), marshalErrors(endpointslicesErrors)) + // Service Accounts servicesAccounts, servicesAccountsErrors := serviceAccounts(ctx, client, namespaceNames) for k, v := range servicesAccounts { @@ -1983,6 +1990,42 @@ func endpoints(ctx context.Context, client *kubernetes.Clientset, namespaces []s return endpointsByNamespace, errorsByNamespace } +func endpointslices(ctx context.Context, client *kubernetes.Clientset, namespaces []string) (map[string][]byte, map[string]string) { + objsByNamespace := make(map[string][]byte) + errorsByNamespace := make(map[string]string) + + for _, namespace := range namespaces { + objs, err := client.DiscoveryV1().EndpointSlices(namespace).List(ctx, metav1.ListOptions{}) + if err != nil { + errorsByNamespace[namespace] = err.Error() + continue + } + + // TODO: Can we DRY this? We repeat this pattern a lot + gvk, err := apiutil.GVKForObject(objs, scheme.Scheme) + if err == nil { + objs.GetObjectKind().SetGroupVersionKind(gvk) + } + + for i, o := range objs.Items { + gvk, err := apiutil.GVKForObject(&o, scheme.Scheme) + if err == nil { + objs.Items[i].GetObjectKind().SetGroupVersionKind(gvk) + } + } + + b, err := json.MarshalIndent(objs, "", " ") + if err != nil { + errorsByNamespace[namespace] = err.Error() + continue + } + + objsByNamespace[namespace+".json"] = b + } + + return objsByNamespace, errorsByNamespace +} + func serviceAccounts(ctx context.Context, client kubernetes.Interface, namespaces []string) (map[string][]byte, map[string]string) { serviceAccountsByNamespace := make(map[string][]byte) errorsByNamespace := make(map[string]string) diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go index b44a53043..1fd9ce3d4 100644 --- a/pkg/constants/constants.go +++ b/pkg/constants/constants.go @@ -56,6 +56,7 @@ const ( CLUSTER_RESOURCES_CLUSTER_ROLE_BINDINGS = "clusterrolebindings" CLUSTER_RESOURCES_PRIORITY_CLASS = "priorityclasses" CLUSTER_RESOURCES_ENDPOINTS = "endpoints" + CLUSTER_RESOURCES_ENDPOINTSICES = "endpointslices" CLUSTER_RESOURCES_SERVICE_ACCOUNTS = "serviceaccounts" CLUSTER_RESOURCES_LEASES = "leases" CLUSTER_RESOURCES_VOLUME_ATTACHMENTS = "volumeattachments" From e7c07a7d79f58a524dfe8fe48a65f94a3991c5b4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 8 Oct 2024 00:22:10 +0000 Subject: [PATCH 03/25] chore(deps): bump the security group with 3 updates (#1639) Bumps the security group with 3 updates: [golang.org/x/net](https://github.com/golang/net), [golang.org/x/sys](https://github.com/golang/sys) and [golang.org/x/text](https://github.com/golang/text). Updates `golang.org/x/net` from 0.29.0 to 0.30.0 - [Commits](https://github.com/golang/net/compare/v0.29.0...v0.30.0) Updates `golang.org/x/sys` from 0.25.0 to 0.26.0 - [Commits](https://github.com/golang/sys/compare/v0.25.0...v0.26.0) Updates `golang.org/x/text` from 0.18.0 to 0.19.0 - [Release notes](https://github.com/golang/text/releases) - [Commits](https://github.com/golang/text/compare/v0.18.0...v0.19.0) --- updated-dependencies: - dependency-name: golang.org/x/net dependency-type: direct:production update-type: version-update:semver-minor dependency-group: security - dependency-name: golang.org/x/sys dependency-type: direct:production update-type: version-update:semver-minor dependency-group: security - dependency-name: golang.org/x/text dependency-type: direct:production update-type: version-update:semver-minor dependency-group: security ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 10 +++++----- go.sum | 20 ++++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/go.mod b/go.mod index ec3e2a2c8..e88a77f61 100644 --- a/go.mod +++ b/go.mod @@ -238,12 +238,12 @@ require ( github.com/yusufpapurcu/wmi v1.2.4 // indirect go.opencensus.io v0.24.0 // indirect go.starlark.net v0.0.0-20230525235612-a134d8f9ddca // indirect - golang.org/x/crypto v0.27.0 // indirect - golang.org/x/net v0.29.0 + golang.org/x/crypto v0.28.0 // indirect + golang.org/x/net v0.30.0 golang.org/x/oauth2 v0.22.0 // indirect - golang.org/x/sys v0.25.0 - golang.org/x/term v0.24.0 // indirect - golang.org/x/text v0.18.0 + golang.org/x/sys v0.26.0 + golang.org/x/term v0.25.0 // indirect + golang.org/x/text v0.19.0 golang.org/x/time v0.5.0 // indirect google.golang.org/api v0.172.0 // indirect google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de // indirect diff --git a/go.sum b/go.sum index a039a8df0..db360a3d0 100644 --- a/go.sum +++ b/go.sum @@ -969,8 +969,8 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= -golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -1062,8 +1062,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= -golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= -golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1185,14 +1185,14 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= -golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM= -golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1203,8 +1203,8 @@ golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= -golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= From 8105fa00e915c4d6dfb9f9186a9047e4c9832f97 Mon Sep 17 00:00:00 2001 From: Diamon Wiggins <38189728+diamonwiggins@users.noreply.github.com> Date: Wed, 9 Oct 2024 01:38:49 -0400 Subject: [PATCH 04/25] Refactor Remote Host Collection (#1633) * refactor remote collectors * add remotecollect params struct * remove commented checkrbac function * removed unused function * add temp comments * refactor to not require RemoteCollect method per collector * removed unneeded param * removed unneeded param * more refactor * more refactor * remove unneeded function * remove debug print * fix analyzer results * move rbac to separate file * be more specific with rbac function name * fix imports * fix node list file * make k8s rest client config consistent with in cluster collection * add ctx and otel tracing * add test for allCollectedData * move runHostCollectorsInPod to spec instead of metadata * make generate * fix broken references to supportbundle metadata * add e2e tests * update loader tests * fix tests * fix hostos remote collector spec * update remoteHostCollectrs.yaml --------- Co-authored-by: Dexter Yan --- cmd/troubleshoot/cli/run.go | 21 +- .../crds/troubleshoot.sh_supportbundles.yaml | 4 + .../v1beta2/remote_collector_shared.go | 1 + .../v1beta2/supportbundle_types.go | 12 +- .../v1beta2/zz_generated.deepcopy.go | 18 +- .../v1beta2/fake/fake_supportbundle.go | 2 +- .../troubleshoot/v1beta2/supportbundle.go | 4 +- pkg/collect/cluster_resources_test.go | 8 +- pkg/collect/host_collector.go | 159 +++++++++++- pkg/collect/host_collector_test.go | 39 +++ pkg/collect/host_os_info.go | 60 ----- pkg/loader/loader_test.go | 22 +- pkg/supportbundle/collect.go | 240 +++++++++++++----- pkg/supportbundle/load.go | 4 +- pkg/supportbundle/rbac.go | 72 ++++++ .../supportbundle-troubleshoot-v1beta2.json | 6 + .../host_remote_collector_e2e_test.go | 41 +++ .../spec/hostOSRemoteCollector.yaml | 2 +- .../spec/remoteHostCollectors.yaml | 34 +++ testdata/yamldocs/helm-template.yaml | 2 +- 20 files changed, 558 insertions(+), 193 deletions(-) create mode 100644 pkg/collect/host_collector_test.go create mode 100644 pkg/supportbundle/rbac.go create mode 100644 test/e2e/support-bundle/host_remote_collector_e2e_test.go create mode 100644 test/e2e/support-bundle/spec/remoteHostCollectors.yaml diff --git a/cmd/troubleshoot/cli/run.go b/cmd/troubleshoot/cli/run.go index f141ed87d..89cfef523 100644 --- a/cmd/troubleshoot/cli/run.go +++ b/cmd/troubleshoot/cli/run.go @@ -110,7 +110,7 @@ func runTroubleshoot(v *viper.Viper, args []string) error { } if interactive { - if len(mainBundle.Spec.HostCollectors) > 0 && !util.IsRunningAsRoot() { + if len(mainBundle.Spec.HostCollectors) > 0 && !util.IsRunningAsRoot() && !mainBundle.Spec.RunHostCollectorsInPod { fmt.Print(cursor.Show()) if util.PromptYesNo(util.HOST_COLLECTORS_RUN_AS_ROOT_PROMPT) { fmt.Println("Exiting...") @@ -184,7 +184,7 @@ func runTroubleshoot(v *viper.Viper, args []string) error { OutputPath: v.GetString("output"), Redact: v.GetBool("redact"), FromCLI: true, - RunHostCollectorsInPod: mainBundle.Metadata.RunHostCollectorsInPod, + RunHostCollectorsInPod: mainBundle.Spec.RunHostCollectorsInPod, } nonInteractiveOutput := analysisOutput{} @@ -199,7 +199,7 @@ func runTroubleshoot(v *viper.Viper, args []string) error { if len(response.AnalyzerResults) > 0 { if interactive { - if err := showInteractiveResults(mainBundle.Metadata.Name, response.AnalyzerResults, response.ArchivePath); err != nil { + if err := showInteractiveResults(mainBundle.Name, response.AnalyzerResults, response.ArchivePath); err != nil { interactive = false } } else { @@ -208,7 +208,7 @@ func runTroubleshoot(v *viper.Viper, args []string) error { } if !response.FileUploaded { - if appName := mainBundle.Metadata.Labels["applicationName"]; appName != "" { + if appName := mainBundle.Labels["applicationName"]; appName != "" { f := `A support bundle for %s has been created in this directory named %s. Please upload it on the Troubleshoot page of the %s Admin Console to begin analysis.` @@ -337,11 +337,8 @@ func loadSpecs(ctx context.Context, args []string, client kubernetes.Interface) APIVersion: "troubleshoot.sh/v1beta2", Kind: "SupportBundle", }, - Metadata: troubleshootv1beta2.SupportBundleMetadata{ - ObjectMeta: metav1.ObjectMeta{ - Name: "merged-support-bundle-spec", - }, - RunHostCollectorsInPod: false, + ObjectMeta: metav1.ObjectMeta{ + Name: "merged-support-bundle-spec", }, } @@ -351,11 +348,11 @@ func loadSpecs(ctx context.Context, args []string, client kubernetes.Interface) sb := sb mainBundle = supportbundle.ConcatSpec(mainBundle, &sb) //check if sb has metadata and if it has RunHostCollectorsInPod set to true - if !reflect.DeepEqual(sb.Metadata.ObjectMeta, metav1.ObjectMeta{}) && sb.Metadata.RunHostCollectorsInPod { - enableRunHostCollectorsInPod = sb.Metadata.RunHostCollectorsInPod + if !reflect.DeepEqual(sb.ObjectMeta, metav1.ObjectMeta{}) && sb.Spec.RunHostCollectorsInPod { + enableRunHostCollectorsInPod = sb.Spec.RunHostCollectorsInPod } } - mainBundle.Metadata.RunHostCollectorsInPod = enableRunHostCollectorsInPod + mainBundle.Spec.RunHostCollectorsInPod = enableRunHostCollectorsInPod for _, c := range kinds.CollectorsV1Beta2 { mainBundle.Spec.Collectors = util.Append(mainBundle.Spec.Collectors, c.Spec.Collectors) diff --git a/config/crds/troubleshoot.sh_supportbundles.yaml b/config/crds/troubleshoot.sh_supportbundles.yaml index 9c77af9d6..220bd1c33 100644 --- a/config/crds/troubleshoot.sh_supportbundles.yaml +++ b/config/crds/troubleshoot.sh_supportbundles.yaml @@ -2114,6 +2114,8 @@ spec: type: string exclude: type: BoolString + image: + type: string namespace: type: string podLaunchOptions: @@ -20313,6 +20315,8 @@ spec: type: object type: object type: array + runHostCollectorsInPod: + type: boolean uri: description: URI optionally defines a location which is the source of this spec to allow updating of the spec at runtime diff --git a/pkg/apis/troubleshoot/v1beta2/remote_collector_shared.go b/pkg/apis/troubleshoot/v1beta2/remote_collector_shared.go index 25c99acce..e6a0e0703 100644 --- a/pkg/apis/troubleshoot/v1beta2/remote_collector_shared.go +++ b/pkg/apis/troubleshoot/v1beta2/remote_collector_shared.go @@ -16,6 +16,7 @@ type RemoteCollectorMeta struct { type RemoteCPU struct { RemoteCollectorMeta `json:",inline" yaml:",inline"` } + type RemoteHostOS struct { RemoteCollectorMeta `json:",inline" yaml:",inline"` } diff --git a/pkg/apis/troubleshoot/v1beta2/supportbundle_types.go b/pkg/apis/troubleshoot/v1beta2/supportbundle_types.go index 4c1a6df39..44e017019 100644 --- a/pkg/apis/troubleshoot/v1beta2/supportbundle_types.go +++ b/pkg/apis/troubleshoot/v1beta2/supportbundle_types.go @@ -20,11 +20,6 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -type SupportBundleMetadata struct { - metav1.ObjectMeta `json:",inline" yaml:",inline"` - RunHostCollectorsInPod bool `json:"runHostCollectorsInPod,omitempty" yaml:"runHostCollectorsInPod,omitempty"` -} - // SupportBundleSpec defines the desired state of SupportBundle type SupportBundleSpec struct { AfterCollection []*AfterCollection `json:"afterCollection,omitempty" yaml:"afterCollection,omitempty"` @@ -33,7 +28,8 @@ type SupportBundleSpec struct { Analyzers []*Analyze `json:"analyzers,omitempty" yaml:"analyzers,omitempty"` HostAnalyzers []*HostAnalyze `json:"hostAnalyzers,omitempty" yaml:"hostAnalyzers,omitempty"` // URI optionally defines a location which is the source of this spec to allow updating of the spec at runtime - Uri string `json:"uri,omitempty" yaml:"uri,omitempty"` + Uri string `json:"uri,omitempty" yaml:"uri,omitempty"` + RunHostCollectorsInPod bool `json:"runHostCollectorsInPod,omitempty" yaml:"runHostCollectorsInPod,omitempty"` } // SupportBundleStatus defines the observed state of SupportBundle @@ -48,8 +44,8 @@ type SupportBundleStatus struct { // SupportBundle is the Schema for the SupportBundles API // +k8s:openapi-gen=true type SupportBundle struct { - metav1.TypeMeta `json:",inline" yaml:",inline"` - Metadata SupportBundleMetadata `json:"metadata,omitempty" yaml:"metadata,omitempty"` + metav1.TypeMeta `json:",inline" yaml:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty" yaml:"metadata,omitempty"` Spec SupportBundleSpec `json:"spec,omitempty" yaml:"spec,omitempty"` Status SupportBundleStatus `json:"status,omitempty"` diff --git a/pkg/apis/troubleshoot/v1beta2/zz_generated.deepcopy.go b/pkg/apis/troubleshoot/v1beta2/zz_generated.deepcopy.go index 6172d11f3..5b40aca8f 100644 --- a/pkg/apis/troubleshoot/v1beta2/zz_generated.deepcopy.go +++ b/pkg/apis/troubleshoot/v1beta2/zz_generated.deepcopy.go @@ -4641,7 +4641,7 @@ func (in *SubnetAvailableAnalyze) DeepCopy() *SubnetAvailableAnalyze { func (in *SupportBundle) DeepCopyInto(out *SupportBundle) { *out = *in out.TypeMeta = in.TypeMeta - in.Metadata.DeepCopyInto(&out.Metadata) + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.Spec.DeepCopyInto(&out.Spec) out.Status = in.Status } @@ -4696,22 +4696,6 @@ func (in *SupportBundleList) DeepCopyObject() runtime.Object { return nil } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *SupportBundleMetadata) DeepCopyInto(out *SupportBundleMetadata) { - *out = *in - in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SupportBundleMetadata. -func (in *SupportBundleMetadata) DeepCopy() *SupportBundleMetadata { - if in == nil { - return nil - } - out := new(SupportBundleMetadata) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *SupportBundleSpec) DeepCopyInto(out *SupportBundleSpec) { *out = *in diff --git a/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta2/fake/fake_supportbundle.go b/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta2/fake/fake_supportbundle.go index 45d40c88c..b28fc4025 100644 --- a/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta2/fake/fake_supportbundle.go +++ b/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta2/fake/fake_supportbundle.go @@ -64,7 +64,7 @@ func (c *FakeSupportBundles) List(ctx context.Context, opts v1.ListOptions) (res } list := &v1beta2.SupportBundleList{ListMeta: obj.(*v1beta2.SupportBundleList).ListMeta} for _, item := range obj.(*v1beta2.SupportBundleList).Items { - if label.Matches(labels.Set(item.Metadata.Labels)) { + if label.Matches(labels.Set(item.Labels)) { list.Items = append(list.Items, item) } } diff --git a/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta2/supportbundle.go b/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta2/supportbundle.go index 521f4df90..49399e767 100644 --- a/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta2/supportbundle.go +++ b/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta2/supportbundle.go @@ -127,7 +127,7 @@ func (c *supportBundles) Update(ctx context.Context, supportBundle *v1beta2.Supp err = c.client.Put(). Namespace(c.ns). Resource("supportbundles"). - Name(supportBundle.Metadata.Name). + Name(supportBundle.Name). VersionedParams(&opts, scheme.ParameterCodec). Body(supportBundle). Do(ctx). @@ -142,7 +142,7 @@ func (c *supportBundles) UpdateStatus(ctx context.Context, supportBundle *v1beta err = c.client.Put(). Namespace(c.ns). Resource("supportbundles"). - Name(supportBundle.Metadata.Name). + Name(supportBundle.Name). SubResource("status"). VersionedParams(&opts, scheme.ParameterCodec). Body(supportBundle). diff --git a/pkg/collect/cluster_resources_test.go b/pkg/collect/cluster_resources_test.go index f8bf6c0a2..d261d02ed 100644 --- a/pkg/collect/cluster_resources_test.go +++ b/pkg/collect/cluster_resources_test.go @@ -481,11 +481,9 @@ func TestCollectClusterResources_CustomResource(t *testing.T) { // Create a CR sbObject := troubleshootv1beta2.SupportBundle{ - Metadata: troubleshootv1beta2.SupportBundleMetadata{ - ObjectMeta: metav1.ObjectMeta{ - Name: "supportbundle", - Namespace: "default", - }, + ObjectMeta: metav1.ObjectMeta{ + Name: "supportbundle", + Namespace: "default", }, TypeMeta: metav1.TypeMeta{ Kind: "SupportBundle", diff --git a/pkg/collect/host_collector.go b/pkg/collect/host_collector.go index 04e96de0a..767313a5d 100644 --- a/pkg/collect/host_collector.go +++ b/pkg/collect/host_collector.go @@ -1,14 +1,43 @@ package collect import ( + "bytes" + "context" + "encoding/json" + "fmt" + "os" + "path/filepath" + "time" + + "github.com/pkg/errors" troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" + "github.com/replicatedhq/troubleshoot/pkg/constants" + "golang.org/x/sync/errgroup" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/storage/names" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" ) type HostCollector interface { Title() string IsExcluded() (bool, error) Collect(progressChan chan<- interface{}) (map[string][]byte, error) - RemoteCollect(progressChan chan<- interface{}) (map[string][]byte, error) // RemoteCollect is used to priviledge pods to collect data from different nodes +} + +type RemoteCollectParams struct { + ProgressChan chan<- interface{} + HostCollector *troubleshootv1beta2.HostCollect + BundlePath string + ClientConfig *rest.Config // specify actual type + Image string + PullPolicy string // specify actual type if needed + Timeout time.Duration // specify duration type if needed + LabelSelector string + NamePrefix string + Namespace string + Title string } func GetHostCollector(collector *troubleshootv1beta2.HostCollect, bundlePath string) (HostCollector, bool) { @@ -81,3 +110,131 @@ func hostCollectorTitleOrDefault(meta troubleshootv1beta2.HostCollectorMeta, def } return defaultTitle } + +func RemoteHostCollect(ctx context.Context, params RemoteCollectParams) (map[string][]byte, error) { + scheme := runtime.NewScheme() + if err := corev1.AddToScheme(scheme); err != nil { + return nil, errors.Wrap(err, "failed to add runtime scheme") + } + + client, err := kubernetes.NewForConfig(params.ClientConfig) + if err != nil { + return nil, err + } + + runner := &podRunner{ + client: client, + scheme: scheme, + image: params.Image, + pullPolicy: params.PullPolicy, + waitInterval: remoteCollectorDefaultInterval, + } + + // Get all the nodes where we should run. + nodes, err := listNodesNamesInSelector(ctx, client, params.LabelSelector) + if err != nil { + return nil, errors.Wrap(err, "failed to get the list of nodes matching a nodeSelector") + } + + if params.NamePrefix == "" { + params.NamePrefix = remoteCollectorNamePrefix + } + + result, err := runRemote(ctx, runner, nodes, params.HostCollector, names.SimpleNameGenerator, params.NamePrefix, params.Namespace) + if err != nil { + return nil, errors.Wrap(err, "failed to run collector remotely") + } + + allCollectedData := mapCollectorResultToOutput(result, params) + output := NewResult() + + // save the first result we find in the node and save it + for node, result := range allCollectedData { + var nodeResult map[string]string + if err := json.Unmarshal(result, &nodeResult); err != nil { + return nil, errors.Wrap(err, "failed to marshal node results") + } + + for file, collectorResult := range nodeResult { + directory := filepath.Dir(file) + fileName := filepath.Base(file) + // expected file name for remote collectors will be the normal path separated by / and the node name + output.SaveResult(params.BundlePath, fmt.Sprintf("%s/%s/%s", directory, node, fileName), bytes.NewBufferString(collectorResult)) + } + } + + // check if NODE_LIST_FILE exists + _, err = os.Stat(constants.NODE_LIST_FILE) + // if it not exists, save the nodes list + if err != nil { + nodesBytes, err := json.MarshalIndent(HostOSInfoNodes{Nodes: nodes}, "", " ") + if err != nil { + return nil, errors.Wrap(err, "failed to marshal host os info nodes") + } + output.SaveResult(params.BundlePath, constants.NODE_LIST_FILE, bytes.NewBuffer(nodesBytes)) + } + return output, nil +} + +func runRemote(ctx context.Context, runner runner, nodes []string, collector *troubleshootv1beta2.HostCollect, nameGenerator names.NameGenerator, namePrefix string, namespace string) (map[string][]byte, error) { + g, ctx := errgroup.WithContext(ctx) + results := make(chan map[string][]byte, len(nodes)) + + for _, node := range nodes { + node := node + g.Go(func() error { + // May need to evaluate error and log warning. Otherwise any error + // here will cancel the context of other goroutines and no results + // will be returned. + return runner.run(ctx, collector, namespace, nameGenerator.GenerateName(namePrefix+"-"), node, results) + }) + } + + // Wait for all collectors to complete or return the first error. + if err := g.Wait(); err != nil { + return nil, errors.Wrap(err, "failed remote collection") + } + close(results) + + output := make(map[string][]byte) + for result := range results { + r := result + for k, v := range r { + output[k] = v + } + } + + return output, nil +} + +func mapCollectorResultToOutput(result map[string][]byte, params RemoteCollectParams) map[string][]byte { + allCollectedData := make(map[string][]byte) + + for k, v := range result { + if curBytes, ok := allCollectedData[k]; ok { + var curResults map[string]string + if err := json.Unmarshal(curBytes, &curResults); err != nil { + params.ProgressChan <- errors.Errorf("failed to read existing results for collector %s: %v\n", params.Title, err) + continue + } + var newResults map[string]string + if err := json.Unmarshal(v, &newResults); err != nil { + params.ProgressChan <- errors.Errorf("failed to read new results for collector %s: %v\n", params.Title, err) + continue + } + for file, data := range newResults { + curResults[file] = data + } + combinedResults, err := json.Marshal(curResults) + if err != nil { + params.ProgressChan <- errors.Errorf("failed to combine results for collector %s: %v\n", params.Title, err) + continue + } + allCollectedData[k] = combinedResults + } else { + allCollectedData[k] = v + } + + } + return allCollectedData +} diff --git a/pkg/collect/host_collector_test.go b/pkg/collect/host_collector_test.go new file mode 100644 index 000000000..3ce88b50e --- /dev/null +++ b/pkg/collect/host_collector_test.go @@ -0,0 +1,39 @@ +package collect + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +type Params struct { + Title string + ProgressChan chan error +} + +// Mock data for testing +var testParams = RemoteCollectParams{ + Title: "Test", + ProgressChan: make(chan interface{}), +} + +func Test_mapCollectorResultToOutput(t *testing.T) { + result := map[string][]byte{ + "key1": []byte(`{"file1": "data1", "file2": "data2"}`), + "key2": []byte(`{"file3": "data3"}`), + } + + // Expected output after processing + expectedCollectedData := map[string][]byte{ + "key1": []byte(`{"file1": "data1", "file2": "data2"}`), + "key2": []byte(`{"file3": "data3"}`), + } + + // Run the function logic + allCollectedData := mapCollectorResultToOutput(result, testParams) + + // Validate the collected data + for key, expected := range expectedCollectedData { + assert.Equal(t, string(expected), string(allCollectedData[key]), "The collected data for key %s is incorrect", key) + } +} diff --git a/pkg/collect/host_os_info.go b/pkg/collect/host_os_info.go index a3c8fa6ed..79fc5ab9b 100644 --- a/pkg/collect/host_os_info.go +++ b/pkg/collect/host_os_info.go @@ -3,11 +3,9 @@ package collect import ( "bytes" "encoding/json" - "fmt" "github.com/pkg/errors" troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" - "github.com/replicatedhq/troubleshoot/pkg/k8sutil" osutils "github.com/shirou/gopsutil/v3/host" ) @@ -60,61 +58,3 @@ func (c *CollectHostOS) Collect(progressChan chan<- interface{}) (map[string][]b return output, nil } - -func (c *CollectHostOS) RemoteCollect(progressChan chan<- interface{}) (map[string][]byte, error) { - restConfig, err := k8sutil.GetRESTConfig() - if err != nil { - return nil, errors.Wrap(err, "failed to convert kube flags to rest config") - } - - createOpts := CollectorRunOpts{ - KubernetesRestConfig: restConfig, - Image: "replicated/troubleshoot:latest", - Namespace: "default", - Timeout: defaultTimeout, - NamePrefix: "hostos-remote", - ProgressChan: progressChan, - } - - remoteCollector := &troubleshootv1beta2.RemoteCollector{ - Spec: troubleshootv1beta2.RemoteCollectorSpec{ - Collectors: []*troubleshootv1beta2.RemoteCollect{ - { - HostOS: &troubleshootv1beta2.RemoteHostOS{}, - }, - }, - }, - } - // empty redactor for now - additionalRedactors := &troubleshootv1beta2.Redactor{} - // re-use the collect.CollectRemote function to run the remote collector - results, err := CollectRemote(remoteCollector, additionalRedactors, createOpts) - if err != nil { - return nil, errors.Wrap(err, "failed to run remote collector") - } - - output := NewResult() - - // save the first result we find in the node and save it - for node, result := range results.AllCollectedData { - var nodeResult map[string]string - if err := json.Unmarshal(result, &nodeResult); err != nil { - return nil, errors.Wrap(err, "failed to marshal node results") - } - - for _, collectorResult := range nodeResult { - var collectedItems HostOSInfo - if err := json.Unmarshal([]byte(collectorResult), &collectedItems); err != nil { - return nil, errors.Wrap(err, "failed to marshal collector results") - } - - b, err := json.MarshalIndent(collectedItems, "", " ") - if err != nil { - return nil, errors.Wrap(err, "failed to marshal host os info") - } - output.SaveResult(c.BundlePath, fmt.Sprintf("host-collectors/system/%s/%s", node, HostInfoFileName), bytes.NewBuffer(b)) - } - } - - return output, nil -} diff --git a/pkg/loader/loader_test.go b/pkg/loader/loader_test.go index beb5feeec..3080b3697 100644 --- a/pkg/loader/loader_test.go +++ b/pkg/loader/loader_test.go @@ -30,11 +30,11 @@ func TestLoadingHelmTemplate_Succeeds(t *testing.T) { // Assert a few fields from the loaded troubleshoot specs assert.Equal(t, "redactor-spec-1", kinds.RedactorsV1Beta2[0].ObjectMeta.Name) assert.Equal(t, "REDACT SECOND TEXT PLEASE", kinds.RedactorsV1Beta2[0].Spec.Redactors[0].Removals.Values[0]) - assert.Equal(t, "sb-spec-1", kinds.SupportBundlesV1Beta2[0].Metadata.Name) - assert.Equal(t, "sb-spec-2", kinds.SupportBundlesV1Beta2[1].Metadata.Name) - assert.Equal(t, "sb-spec-3", kinds.SupportBundlesV1Beta2[2].Metadata.Name) - assert.Equal(t, false, kinds.SupportBundlesV1Beta2[0].Metadata.RunHostCollectorsInPod) - assert.Equal(t, true, kinds.SupportBundlesV1Beta2[2].Metadata.RunHostCollectorsInPod) + assert.Equal(t, "sb-spec-1", kinds.SupportBundlesV1Beta2[0].Name) + assert.Equal(t, "sb-spec-2", kinds.SupportBundlesV1Beta2[1].Name) + assert.Equal(t, "sb-spec-3", kinds.SupportBundlesV1Beta2[2].Name) + assert.Equal(t, false, kinds.SupportBundlesV1Beta2[0].Spec.RunHostCollectorsInPod) + assert.Equal(t, true, kinds.SupportBundlesV1Beta2[2].Spec.RunHostCollectorsInPod) assert.Equal(t, "wg-easy", kinds.SupportBundlesV1Beta2[1].Spec.Collectors[0].Logs.CollectorName) assert.Equal(t, "Node Count Check", kinds.PreflightsV1Beta2[0].Spec.Analyzers[0].NodeResources.CheckName) assert.Len(t, kinds.PreflightsV1Beta2[0].Spec.Collectors, 0) @@ -348,10 +348,8 @@ func TestLoadingMultidocsWithTroubleshootSpecs(t *testing.T) { Kind: "SupportBundle", APIVersion: "troubleshoot.sh/v1beta2", }, - Metadata: troubleshootv1beta2.SupportBundleMetadata{ - ObjectMeta: metav1.ObjectMeta{ - Name: "my-support-bundle", - }, + ObjectMeta: metav1.ObjectMeta{ + Name: "my-support-bundle", }, Spec: troubleshootv1beta2.SupportBundleSpec{ Collectors: []*troubleshootv1beta2.Collect{ @@ -630,10 +628,8 @@ func TestLoadingEmptySpec(t *testing.T) { Kind: "SupportBundle", APIVersion: "troubleshoot.sh/v1beta2", }, - Metadata: troubleshootv1beta2.SupportBundleMetadata{ - ObjectMeta: metav1.ObjectMeta{ - Name: "empty", - }, + ObjectMeta: metav1.ObjectMeta{ + Name: "empty", }, }, }, diff --git a/pkg/supportbundle/collect.go b/pkg/supportbundle/collect.go index a839ca0d9..6cff37701 100644 --- a/pkg/supportbundle/collect.go +++ b/pkg/supportbundle/collect.go @@ -8,6 +8,7 @@ import ( "io" "os" "reflect" + "time" "github.com/pkg/errors" analyze "github.com/replicatedhq/troubleshoot/pkg/analyze" @@ -22,89 +23,52 @@ import ( "k8s.io/client-go/kubernetes" ) -func runHostCollectors(ctx context.Context, hostCollectors []*troubleshootv1beta2.HostCollect, additionalRedactors *troubleshootv1beta2.Redactor, bundlePath string, opts SupportBundleCreateOpts) (collect.CollectorResult, error) { - collectSpecs := make([]*troubleshootv1beta2.HostCollect, 0) - collectSpecs = append(collectSpecs, hostCollectors...) +type FilteredCollector struct { + Spec troubleshootv1beta2.HostCollect + Collector collect.HostCollector +} - allCollectedData := make(map[string][]byte) +func runHostCollectors(ctx context.Context, hostCollectors []*troubleshootv1beta2.HostCollect, additionalRedactors *troubleshootv1beta2.Redactor, bundlePath string, opts SupportBundleCreateOpts) (collect.CollectorResult, error) { + collectSpecs := append([]*troubleshootv1beta2.HostCollect{}, hostCollectors...) + collectedData := make(map[string][]byte) - var collectors []collect.HostCollector - for _, desiredCollector := range collectSpecs { - collector, ok := collect.GetHostCollector(desiredCollector, bundlePath) - if ok { - collectors = append(collectors, collector) - } + // Filter out excluded collectors + filteredCollectors, err := filterHostCollectors(ctx, collectSpecs, bundlePath, opts) + if err != nil { + return nil, err } - for _, collector := range collectors { - // TODO: Add context to host collectors - _, span := otel.Tracer(constants.LIB_TRACER_NAME).Start(ctx, collector.Title()) - span.SetAttributes(attribute.String("type", reflect.TypeOf(collector).String())) - - isExcluded, _ := collector.IsExcluded() - if isExcluded { - opts.ProgressChan <- fmt.Sprintf("[%s] Excluding host collector", collector.Title()) - span.SetAttributes(attribute.Bool(constants.EXCLUDED, true)) - span.End() - continue - } + if opts.RunHostCollectorsInPod { + if err := checkRemoteCollectorRBAC(ctx, opts.KubernetesRestConfig, "Remote Host Collectors", opts.Namespace); err != nil { + if rbacErr, ok := err.(*RBACPermissionError); ok { + for _, forbiddenErr := range rbacErr.Forbidden { + opts.ProgressChan <- forbiddenErr + } - opts.ProgressChan <- fmt.Sprintf("[%s] Running host collector...", collector.Title()) - if opts.RunHostCollectorsInPod { - result, err := collector.RemoteCollect(opts.ProgressChan) - if err != nil { - // If the collector does not have a remote collector implementation, try to run it locally - if errors.Is(err, collect.ErrRemoteCollectorNotImplemented) { - result, err = collector.Collect(opts.ProgressChan) - if err != nil { - span.SetStatus(codes.Error, err.Error()) - opts.ProgressChan <- errors.Errorf("failed to run host collector: %s: %v", collector.Title(), err) - } - } else { - // If the collector has a remote collector implementation, but it failed to run, return the error - span.SetStatus(codes.Error, err.Error()) - opts.ProgressChan <- errors.Errorf("failed to run host collector: %s: %v", collector.Title(), err) + if !opts.CollectWithoutPermissions { + return nil, collect.ErrInsufficientPermissionsToRun } - } - // If the collector has a remote collector implementation, and it ran successfully, return the result - span.End() - for k, v := range result { - allCollectedData[k] = v - } - } else { - // If the collector does not enable run host collectors in pod, run it locally - result, err := collector.Collect(opts.ProgressChan) - if err != nil { - span.SetStatus(codes.Error, err.Error()) - opts.ProgressChan <- errors.Errorf("failed to run host collector: %s: %v", collector.Title(), err) - } - span.End() - for k, v := range result { - allCollectedData[k] = v + } else { + return nil, err } } - } - - collectResult := allCollectedData - - globalRedactors := []*troubleshootv1beta2.Redact{} - if additionalRedactors != nil { - globalRedactors = additionalRedactors.Spec.Redactors + if err := collectRemoteHost(ctx, filteredCollectors, bundlePath, opts, collectedData); err != nil { + return nil, err + } + } else { + if err := collectHost(ctx, filteredCollectors, opts, collectedData); err != nil { + return nil, err + } } if opts.Redact { - _, span := otel.Tracer(constants.LIB_TRACER_NAME).Start(ctx, "Host collectors") - span.SetAttributes(attribute.String("type", "Redactors")) - err := collect.RedactResult(bundlePath, collectResult, globalRedactors) - if err != nil { - err = errors.Wrap(err, "failed to redact host collector results") - span.SetStatus(codes.Error, err.Error()) - return collectResult, err + globalRedactors := getGlobalRedactors(additionalRedactors) + if err := redactResults(ctx, bundlePath, collectedData, globalRedactors); err != nil { + return collectedData, err } - span.End() } - return collectResult, nil + return collectedData, nil } func runCollectors(ctx context.Context, collectors []*troubleshootv1beta2.Collect, additionalRedactors *troubleshootv1beta2.Redactor, bundlePath string, opts SupportBundleCreateOpts) (collect.CollectorResult, error) { @@ -253,3 +217,141 @@ func getAnalysisFile(analyzeResults []*analyze.AnalyzeResult) (io.Reader, error) return bytes.NewBuffer(analysis), nil } + +// collectRemoteHost runs remote host collectors sequentially +func collectRemoteHost(ctx context.Context, filteredCollectors []FilteredCollector, bundlePath string, opts SupportBundleCreateOpts, collectedData map[string][]byte) error { + opts.KubernetesRestConfig.QPS = constants.DEFAULT_CLIENT_QPS + opts.KubernetesRestConfig.Burst = constants.DEFAULT_CLIENT_BURST + opts.KubernetesRestConfig.UserAgent = fmt.Sprintf("%s/%s", constants.DEFAULT_CLIENT_USER_AGENT, version.Version()) + + // Run remote collectors sequentially + for _, c := range filteredCollectors { + collector := c.Collector + spec := c.Spec + + // Send progress event: starting the collector + opts.ProgressChan <- fmt.Sprintf("[%s] Running host collector...", collector.Title()) + + // Start a span for tracing + _, span := otel.Tracer(constants.LIB_TRACER_NAME).Start(ctx, collector.Title()) + span.SetAttributes(attribute.String("type", reflect.TypeOf(collector).String())) + + // Parameters for remote collection + params := &collect.RemoteCollectParams{ + ProgressChan: opts.ProgressChan, + HostCollector: &spec, + BundlePath: bundlePath, + ClientConfig: opts.KubernetesRestConfig, + Image: "replicated/troubleshoot:latest", + PullPolicy: "IfNotPresent", + Timeout: time.Duration(60 * time.Second), + LabelSelector: "", + NamePrefix: "host-remote", + Namespace: "default", + Title: collector.Title(), + } + + // Perform the collection + result, err := collect.RemoteHostCollect(ctx, *params) + if err != nil { + span.SetStatus(codes.Error, err.Error()) + opts.ProgressChan <- fmt.Sprintf("[%s] Error: %v", collector.Title(), err) + return errors.Wrap(err, "failed to run remote host collector") + } + + // Send progress event: completed successfully + opts.ProgressChan <- fmt.Sprintf("[%s] Completed host collector", collector.Title()) + + // Aggregate the results + for k, v := range result { + collectedData[k] = v + } + + span.End() + } + return nil +} + +// collectHost runs host collectors sequentially +func collectHost(ctx context.Context, filteredCollectors []FilteredCollector, opts SupportBundleCreateOpts, collectedData map[string][]byte) error { + // Run local collectors sequentially + for _, c := range filteredCollectors { + collector := c.Collector + + // Send progress event: starting the collector + opts.ProgressChan <- fmt.Sprintf("[%s] Running host collector...", collector.Title()) + + // Start a span for tracing + _, span := otel.Tracer(constants.LIB_TRACER_NAME).Start(ctx, collector.Title()) + span.SetAttributes(attribute.String("type", reflect.TypeOf(collector).String())) + + // Run local collector sequentially + result, err := collector.Collect(opts.ProgressChan) + if err != nil { + span.SetStatus(codes.Error, err.Error()) + opts.ProgressChan <- fmt.Sprintf("[%s] Error: %v", collector.Title(), err) + return errors.Wrap(err, "failed to run host collector") + } + + // Send progress event: completed successfully + opts.ProgressChan <- fmt.Sprintf("[%s] Completed host collector", collector.Title()) + + // Aggregate the results + for k, v := range result { + collectedData[k] = v + } + + span.End() + } + return nil +} + +func redactResults(ctx context.Context, bundlePath string, collectedData collect.CollectorResult, redactors []*troubleshootv1beta2.Redact) error { + _, span := otel.Tracer(constants.LIB_TRACER_NAME).Start(ctx, "Host collectors") + defer span.End() + + err := collect.RedactResult(bundlePath, collectedData, redactors) + if err != nil { + span.SetStatus(codes.Error, err.Error()) + return errors.Wrap(err, "failed to redact host collector results") + } + return nil +} + +// getGlobalRedactors returns the global redactors from the support bundle spec +func getGlobalRedactors(additionalRedactors *troubleshootv1beta2.Redactor) []*troubleshootv1beta2.Redact { + if additionalRedactors != nil { + return additionalRedactors.Spec.Redactors + } + return []*troubleshootv1beta2.Redact{} +} + +// filterHostCollectors filters out excluded collectors and returns a list of collectors to run +func filterHostCollectors(ctx context.Context, collectSpecs []*troubleshootv1beta2.HostCollect, bundlePath string, opts SupportBundleCreateOpts) ([]FilteredCollector, error) { + var filteredCollectors []FilteredCollector + + for _, desiredCollector := range collectSpecs { + collector, ok := collect.GetHostCollector(desiredCollector, bundlePath) + _, span := otel.Tracer(constants.LIB_TRACER_NAME).Start(ctx, collector.Title()) + span.SetAttributes(attribute.String("type", reflect.TypeOf(collector).String())) + + if !ok { + return nil, collect.ErrHostCollectorNotFound + } + + isExcluded, _ := collector.IsExcluded() + if isExcluded { + opts.ProgressChan <- fmt.Sprintf("[%s] Excluding host collector", collector.Title()) + span.SetAttributes(attribute.Bool(constants.EXCLUDED, true)) + span.End() + continue + } + + filteredCollectors = append(filteredCollectors, FilteredCollector{ + Spec: *desiredCollector, + Collector: collector, + }) + } + + return filteredCollectors, nil +} diff --git a/pkg/supportbundle/load.go b/pkg/supportbundle/load.go index 6b61546cc..bbbaf275f 100644 --- a/pkg/supportbundle/load.go +++ b/pkg/supportbundle/load.go @@ -62,9 +62,7 @@ func ParseSupportBundle(doc []byte, followURI bool) (*troubleshootv1beta2.Suppor APIVersion: "troubleshoot.sh/v1beta2", Kind: "SupportBundle", }, - Metadata: troubleshootv1beta2.SupportBundleMetadata{ - ObjectMeta: collector.ObjectMeta, - }, + ObjectMeta: collector.ObjectMeta, Spec: troubleshootv1beta2.SupportBundleSpec{ Collectors: collector.Spec.Collectors, Analyzers: []*troubleshootv1beta2.Analyze{}, diff --git a/pkg/supportbundle/rbac.go b/pkg/supportbundle/rbac.go new file mode 100644 index 000000000..0ed58a9fa --- /dev/null +++ b/pkg/supportbundle/rbac.go @@ -0,0 +1,72 @@ +package supportbundle + +import ( + "context" + "fmt" + + "github.com/pkg/errors" + "github.com/replicatedhq/troubleshoot/pkg/collect" + authorizationv1 "k8s.io/api/authorization/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" +) + +// Custom error type for RBAC permission errors +type RBACPermissionError struct { + Forbidden []error +} + +func (e *RBACPermissionError) Error() string { + return fmt.Sprintf("insufficient permissions: %v", e.Forbidden) +} + +func (e *RBACPermissionError) HasErrors() bool { + return len(e.Forbidden) > 0 +} + +// checkRBAC checks if the current user has the necessary permissions to run the collectors +func checkRemoteCollectorRBAC(ctx context.Context, clientConfig *rest.Config, title string, namespace string) error { + client, err := kubernetes.NewForConfig(clientConfig) + if err != nil { + return errors.Wrap(err, "failed to create client from config") + } + + var forbidden []error + + spec := authorizationv1.SelfSubjectAccessReviewSpec{ + ResourceAttributes: &authorizationv1.ResourceAttributes{ + Namespace: namespace, + Verb: "create,delete", + Group: "", + Version: "", + Resource: "pods,configmap", + Subresource: "", + Name: "", + }, + NonResourceAttributes: nil, + } + + sar := &authorizationv1.SelfSubjectAccessReview{ + Spec: spec, + } + resp, err := client.AuthorizationV1().SelfSubjectAccessReviews().Create(ctx, sar, metav1.CreateOptions{}) + if err != nil { + return errors.Wrap(err, "failed to run subject review") + } + + if !resp.Status.Allowed { + forbidden = append(forbidden, collect.RBACError{ + DisplayName: title, + Namespace: spec.ResourceAttributes.Namespace, + Resource: spec.ResourceAttributes.Resource, + Verb: spec.ResourceAttributes.Verb, + }) + } + + if len(forbidden) > 0 { + return &RBACPermissionError{Forbidden: forbidden} + } + + return nil +} diff --git a/schemas/supportbundle-troubleshoot-v1beta2.json b/schemas/supportbundle-troubleshoot-v1beta2.json index 8c46f6b7c..884ac10f6 100644 --- a/schemas/supportbundle-troubleshoot-v1beta2.json +++ b/schemas/supportbundle-troubleshoot-v1beta2.json @@ -3194,6 +3194,9 @@ "exclude": { "oneOf": [{"type": "string"},{"type": "boolean"}] }, + "image": { + "type": "string" + }, "namespace": { "type": "string" }, @@ -19630,6 +19633,9 @@ } } }, + "runHostCollectorsInPod": { + "type": "boolean" + }, "uri": { "description": "URI optionally defines a location which is the source of this spec to allow updating of the spec at runtime", "type": "string" diff --git a/test/e2e/support-bundle/host_remote_collector_e2e_test.go b/test/e2e/support-bundle/host_remote_collector_e2e_test.go new file mode 100644 index 000000000..f8538e254 --- /dev/null +++ b/test/e2e/support-bundle/host_remote_collector_e2e_test.go @@ -0,0 +1,41 @@ +package e2e + +import ( + "bytes" + "context" + "fmt" + "os" + "os/exec" + "testing" + + "sigs.k8s.io/e2e-framework/pkg/envconf" + "sigs.k8s.io/e2e-framework/pkg/features" +) + +func TestHostRemoteCollector(t *testing.T) { + feature := features.New("Host OS Remote Collector Test"). + Assess("run support bundle command successfully", func(ctx context.Context, t *testing.T, c *envconf.Config) context.Context { + var out bytes.Buffer + supportbundleName := "host-os-remote-collector" + cmd := exec.CommandContext(ctx, sbBinary(), "spec/remoteHostCollectors.yaml", "--interactive=false", fmt.Sprintf("-o=%s", supportbundleName)) + cmd.Stdout = &out + err := cmd.Run() + if err != nil { + t.Fatalf("Failed to run the binary: %v", err) + } + + defer func() { + err := os.Remove(fmt.Sprintf("%s.tar.gz", supportbundleName)) + if err != nil { + t.Fatalf("Error removing file: %v", err) + } + }() + + // At this point, we only care that the binary ran successfully, no need to check folder contents. + t.Logf("Binary executed successfully: %s", out.String()) + + return ctx + }).Feature() + + testenv.Test(t, feature) +} diff --git a/test/e2e/support-bundle/spec/hostOSRemoteCollector.yaml b/test/e2e/support-bundle/spec/hostOSRemoteCollector.yaml index 15c9ae807..48b6e4242 100644 --- a/test/e2e/support-bundle/spec/hostOSRemoteCollector.yaml +++ b/test/e2e/support-bundle/spec/hostOSRemoteCollector.yaml @@ -2,7 +2,7 @@ apiVersion: troubleshoot.sh/v1beta2 kind: SupportBundle metadata: name: sb - runHostCollectorsInPod: true # default is false spec: + runHostCollectorsInPod: true hostCollectors: - hostOS: {} diff --git a/test/e2e/support-bundle/spec/remoteHostCollectors.yaml b/test/e2e/support-bundle/spec/remoteHostCollectors.yaml new file mode 100644 index 000000000..698c1ec80 --- /dev/null +++ b/test/e2e/support-bundle/spec/remoteHostCollectors.yaml @@ -0,0 +1,34 @@ +apiVersion: troubleshoot.sh/v1beta2 +kind: SupportBundle +metadata: + name: "remote-host-collectors" +spec: + runHostCollectorsInPod: true + hostCollectors: + - ipv4Interfaces: {} + - hostServices: {} + - cpu: {} + - hostOS: {} + - memory: {} + - blockDevices: {} + - kernelConfigs: {} + - copy: + collectorName: etc-resolv + path: /etc/resolv.conf + - dns: + collectorName: replicated-app-resolve + hostnames: + - replicated.app + - diskUsage: + collectorName: root-disk-usage + path: / + - diskUsage: + collectorName: tmp + path: /tmp + - http: + collectorName: get-replicated-app + get: + url: https://replicated.app + - run: + collectorName: uptime + command: uptime diff --git a/testdata/yamldocs/helm-template.yaml b/testdata/yamldocs/helm-template.yaml index 56cb856bd..ae9b76fff 100644 --- a/testdata/yamldocs/helm-template.yaml +++ b/testdata/yamldocs/helm-template.yaml @@ -47,8 +47,8 @@ data: kind: SupportBundle metadata: name: sb-spec-3 - runHostCollectorsInPod: true spec: + runHostCollectorsInPod: true collectors: - logs: collectorName: wg-easy From 402d1117458ee4e7bacf66d3d4662ad1b00389f0 Mon Sep 17 00:00:00 2001 From: Evans Mungai Date: Wed, 9 Oct 2024 14:03:23 -0500 Subject: [PATCH 05/25] chore: Explicitly run make lint (#1645) * chore: Explicitly run make lint - Install golangci-lint make target to use in action - Do not run go fmt when generating schemas - Increase golangci timeout Signed-off-by: Evans Mungai * Add setup env step Signed-off-by: Evans Mungai * Add build-tags to golangci-lint command Signed-off-by: Evans Mungai --------- Signed-off-by: Evans Mungai --- .github/workflows/build-test-deploy.yaml | 18 ++++++++++-------- .github/workflows/license.yaml | 9 --------- .golangci.yaml | 2 +- Makefile | 20 +++++++++++++------- 4 files changed, 24 insertions(+), 25 deletions(-) diff --git a/.github/workflows/build-test-deploy.yaml b/.github/workflows/build-test-deploy.yaml index aaa83179f..0849d49a8 100644 --- a/.github/workflows/build-test-deploy.yaml +++ b/.github/workflows/build-test-deploy.yaml @@ -22,30 +22,32 @@ concurrency: jobs: fail_if_pull_request_is_draft: if: github.event.pull_request.draft == true - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: - name: Fails in order to indicate that pull request needs to be marked as ready to review and unit tests workflow needs to pass. run: exit 1 test: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 - uses: actions/setup-go@v5 with: go-version: "1.22" - - name: setup env run: | echo "GOPATH=$(go env GOPATH)" >> $GITHUB_ENV echo "$(go env GOPATH)/bin" >> $GITHUB_PATH shell: bash - - - uses: actions/checkout@v4 - - - run: make test + - name: Run linters + run: make install-golangci-lint lint + - name: Run tests + run: make test test-integration: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: - uses: actions/setup-go@v5 with: diff --git a/.github/workflows/license.yaml b/.github/workflows/license.yaml index 92a8048ff..9d61c6957 100644 --- a/.github/workflows/license.yaml +++ b/.github/workflows/license.yaml @@ -14,15 +14,6 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - - name: Setup Go - uses: actions/setup-go@v5 - with: - go-version: 1.21 - - - name: Install Go deps - run: go mod download - - name: Install trivy run: | wget https://github.com/aquasecurity/trivy/releases/download/v${TRIVY_VERSION}/trivy_${TRIVY_VERSION}_Linux-64bit.deb diff --git a/.golangci.yaml b/.golangci.yaml index d2a992443..a80b6564c 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -2,7 +2,7 @@ run: allow-parallel-runners: true - timeout: 30s + timeout: 10m linters: enable: diff --git a/Makefile b/Makefile index e85f845f6..02593d246 100644 --- a/Makefile +++ b/Makefile @@ -7,6 +7,7 @@ VERSION_PACKAGE = github.com/replicatedhq/troubleshoot/pkg/version VERSION ?=`git describe --tags --dirty` DATE=`date -u +"%Y-%m-%dT%H:%M:%SZ"` RUN?="" +GOLANGCI_LINT_VERSION ?= "v1.61.0" GIT_TREE = $(shell git rev-parse --is-inside-work-tree 2>/dev/null) ifneq "$(GIT_TREE)" "" @@ -34,7 +35,8 @@ define LDFLAGS " endef -BUILDFLAGS = -tags "netgo containers_image_ostree_stub exclude_graphdriver_devicemapper exclude_graphdriver_btrfs containers_image_openpgp" -installsuffix netgo +BUILDTAGS = "netgo containers_image_ostree_stub exclude_graphdriver_devicemapper exclude_graphdriver_btrfs containers_image_openpgp" +BUILDFLAGS = -tags ${BUILDTAGS} -installsuffix netgo BUILDPATHS = ./pkg/... ./cmd/... ./internal/... E2EPATHS = ./test/e2e/... TESTFLAGS ?= -v -coverprofile cover.out @@ -152,7 +154,7 @@ check-schemas: generate schemas fi .PHONY: schemas -schemas: fmt vet openapischema bin/schemagen +schemas: openapischema bin/schemagen ./bin/schemagen --output-dir ./schemas bin/schemagen: @@ -236,12 +238,16 @@ scan: ./ .PHONY: lint -lint: - golangci-lint run --new -c .golangci.yaml ${BUILDPATHS} +lint: vet + golangci-lint run --new -c .golangci.yaml --build-tags ${BUILDTAGS} ${BUILDPATHS} -.PHONY: fmt lint-and-fix -lint-and-fix: - golangci-lint run --new --fix -c .golangci.yaml ${BUILDPATHS} +.PHONY: lint-and-fix +lint-and-fix: fmt vet + golangci-lint run --new --fix -c .golangci.yaml --build-tags ${BUILDTAGS} ${BUILDPATHS} + +.PHONY: install-golangci-lint +install-golangci-lint: + go install github.com/golangci/golangci-lint/cmd/golangci-lint@${GOLANGCI_LINT_VERSION} .PHONY: watch watch: npm-install From 52efd167ad1528bcabe48c62a258df49060649b0 Mon Sep 17 00:00:00 2001 From: Shubhag Saxena Date: Thu, 10 Oct 2024 18:59:22 +0530 Subject: [PATCH 06/25] feat: allow users to check cpu arch (#1644) --- pkg/analyze/host_cpu.go | 31 ++++++++++++++++++++++++++----- pkg/analyze/host_cpu_test.go | 9 ++++++++- pkg/collect/host_cpu.go | 7 +++++++ pkg/collect/host_cpu_test.go | 3 ++- 4 files changed, 43 insertions(+), 7 deletions(-) diff --git a/pkg/analyze/host_cpu.go b/pkg/analyze/host_cpu.go index 1dba635cf..97d9f04e2 100644 --- a/pkg/analyze/host_cpu.go +++ b/pkg/analyze/host_cpu.go @@ -63,7 +63,7 @@ func (a *AnalyzeHostCPU) Analyze( return []*AnalyzeResult{&result}, nil } - isMatch, err := compareHostCPUConditionalToActual(outcome.Fail.When, cpuInfo.LogicalCount, cpuInfo.PhysicalCount, cpuInfo.Flags) + isMatch, err := compareHostCPUConditionalToActual(outcome.Fail.When, cpuInfo.LogicalCount, cpuInfo.PhysicalCount, cpuInfo.Flags, cpuInfo.MachineArch) if err != nil { return nil, errors.Wrap(err, "failed to compare") } @@ -84,7 +84,7 @@ func (a *AnalyzeHostCPU) Analyze( return []*AnalyzeResult{&result}, nil } - isMatch, err := compareHostCPUConditionalToActual(outcome.Warn.When, cpuInfo.LogicalCount, cpuInfo.PhysicalCount, cpuInfo.Flags) + isMatch, err := compareHostCPUConditionalToActual(outcome.Warn.When, cpuInfo.LogicalCount, cpuInfo.PhysicalCount, cpuInfo.Flags, cpuInfo.MachineArch) if err != nil { return nil, errors.Wrap(err, "failed to compare") } @@ -105,7 +105,7 @@ func (a *AnalyzeHostCPU) Analyze( return []*AnalyzeResult{&result}, nil } - isMatch, err := compareHostCPUConditionalToActual(outcome.Pass.When, cpuInfo.LogicalCount, cpuInfo.PhysicalCount, cpuInfo.Flags) + isMatch, err := compareHostCPUConditionalToActual(outcome.Pass.When, cpuInfo.LogicalCount, cpuInfo.PhysicalCount, cpuInfo.Flags, cpuInfo.MachineArch) if err != nil { return nil, errors.Wrap(err, "failed to compare") } @@ -164,14 +164,19 @@ func doCompareHostCPUFlags(expected string, flags []string) (res bool, err error return true, nil } -func compareHostCPUConditionalToActual(conditional string, logicalCount int, physicalCount int, flags []string) (res bool, err error) { +func compareHostCPUConditionalToActual(conditional string, logicalCount int, physicalCount int, flags []string, machineArch string) (res bool, err error) { compareLogical := false comparePhysical := false compareUnspecified := false + compareMachineArch := false comparator := "" desired := "" + /* When the conditional is in the format of "logical " + example: when: "count < 2" + */ + parts := strings.Split(conditional, " ") if len(parts) == 3 { comparator = parts[1] @@ -182,6 +187,8 @@ func compareHostCPUConditionalToActual(conditional string, logicalCount int, phy comparePhysical = true } else if strings.ToLower(parts[0]) == "count" { compareUnspecified = true + } else if strings.ToLower(parts[0]) == "machinearch" { + compareMachineArch = true } } else if len(parts) == 2 { compareUnspecified = true @@ -199,7 +206,7 @@ func compareHostCPUConditionalToActual(conditional string, logicalCount int, phy return doCompareHostCPUFlags(desired, flags) } - if !compareLogical && !comparePhysical && !compareUnspecified { + if !compareLogical && !comparePhysical && !compareUnspecified && !compareMachineArch { return false, errors.New("unable to parse conditional") } @@ -207,6 +214,8 @@ func compareHostCPUConditionalToActual(conditional string, logicalCount int, phy return doCompareHostCPU(comparator, desired, logicalCount) } else if comparePhysical { return doCompareHostCPU(comparator, desired, physicalCount) + } else if compareMachineArch { + return doCompareMachineArch(comparator, desired, machineArch) } else { actual := logicalCount if physicalCount > logicalCount { @@ -217,6 +226,16 @@ func compareHostCPUConditionalToActual(conditional string, logicalCount int, phy } } +func doCompareMachineArch(operator string, desired string, actual string) (bool, error) { + switch operator { + case "=", "==", "===": + return actual == desired, nil + case "!=", "!==": + return actual != desired, nil + } + return false, errors.New("unknown operator") +} + func doCompareHostCPU(operator string, desired string, actual int) (bool, error) { desiredInt, err := strconv.ParseInt(desired, 10, 64) if err != nil { @@ -234,6 +253,8 @@ func doCompareHostCPU(operator string, desired string, actual int) (bool, error) return actual >= int(desiredInt), nil case "=", "==", "===": return actual == int(desiredInt), nil + case "!=", "!==": + return actual != int(desiredInt), nil } return false, errors.New("unknown operator") diff --git a/pkg/analyze/host_cpu_test.go b/pkg/analyze/host_cpu_test.go index 316ee7a37..05ba22340 100644 --- a/pkg/analyze/host_cpu_test.go +++ b/pkg/analyze/host_cpu_test.go @@ -82,6 +82,7 @@ func Test_compareHostCPUConditionalToActual(t *testing.T) { logicalCount int physicalCount int flags []string + machineArch string expected bool }{ { @@ -164,12 +165,18 @@ func Test_compareHostCPUConditionalToActual(t *testing.T) { flags: []string{"a", "b", "c", "d", "e"}, expected: true, }, + { + name: "machine arch matches", + when: "machineArch == x86_64", + machineArch: "x86_64", + expected: true, + }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { req := require.New(t) - actual, err := compareHostCPUConditionalToActual(test.when, test.logicalCount, test.physicalCount, test.flags) + actual, err := compareHostCPUConditionalToActual(test.when, test.logicalCount, test.physicalCount, test.flags, test.machineArch) req.NoError(err) assert.Equal(t, test.expected, actual) diff --git a/pkg/collect/host_cpu.go b/pkg/collect/host_cpu.go index d35cb00f6..4e7bb1ed8 100644 --- a/pkg/collect/host_cpu.go +++ b/pkg/collect/host_cpu.go @@ -7,12 +7,14 @@ import ( "github.com/pkg/errors" troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" "github.com/shirou/gopsutil/v3/cpu" + "github.com/shirou/gopsutil/v3/host" ) type CPUInfo struct { LogicalCount int `json:"logicalCount"` PhysicalCount int `json:"physicalCount"` Flags []string `json:"flags"` + MachineArch string `json:"machineArch"` } const HostCPUPath = `host-collectors/system/cpu.json` @@ -45,6 +47,11 @@ func (c *CollectHostCPU) Collect(progressChan chan<- interface{}) (map[string][] } cpuInfo.PhysicalCount = physicalCount + cpuInfo.MachineArch, err = host.KernelArch() + if err != nil { + return nil, errors.Wrap(err, "failed to fetch cpu architecture") + } + // XXX even though the cpu.Info() returns a slice per CPU it is way // common to have the same flags for all CPUs. We consolidate them here // so the output is a list of all different flags present in all CPUs. diff --git a/pkg/collect/host_cpu_test.go b/pkg/collect/host_cpu_test.go index 3c5d6f9da..7e1b831b8 100644 --- a/pkg/collect/host_cpu_test.go +++ b/pkg/collect/host_cpu_test.go @@ -25,8 +25,9 @@ func TestCollectHostCPU_Collect(t *testing.T) { require.NoError(t, err) // Check if values exist. They will be different on different machines. - assert.Equal(t, 3, len(m)) + assert.Equal(t, 4, len(m)) assert.Contains(t, m, "logicalCount") assert.Contains(t, m, "physicalCount") assert.Contains(t, m, "flags") + assert.Contains(t, m, "machineArch") } From 0113624352eba7ab45c4f78e6bdd9913cdf94c85 Mon Sep 17 00:00:00 2001 From: Evans Mungai Date: Fri, 11 Oct 2024 12:48:32 -0500 Subject: [PATCH 07/25] chore(support-bundle): respect using load-cluster-specs=false (#1634) * fix: Allow using load-cluster-specs=false Signed-off-by: Evans Mungai * Some more simplification Signed-off-by: Evans Mungai * Ensure error in loading specs is printed in CLI Signed-off-by: Evans Mungai * Run linter Signed-off-by: Evans Mungai * Fix failing tests Signed-off-by: Evans Mungai * Remove unnecessary test case rename Signed-off-by: Evans Mungai * Fix error wrapping Signed-off-by: Evans Mungai * Check if load-cluster-specs was provided in cli Signed-off-by: Evans Mungai * Better wording in comments Signed-off-by: Evans Mungai --------- Signed-off-by: Evans Mungai --- cmd/troubleshoot/cli/root.go | 14 +++++++++++++- cmd/troubleshoot/cli/run.go | 26 +++++++------------------- internal/specs/specs.go | 2 ++ pkg/loader/loader.go | 28 ++++++++++++++-------------- pkg/types/types.go | 7 ++++++- 5 files changed, 42 insertions(+), 35 deletions(-) diff --git a/cmd/troubleshoot/cli/root.go b/cmd/troubleshoot/cli/root.go index 78b3cc2b9..f2cd03901 100644 --- a/cmd/troubleshoot/cli/root.go +++ b/cmd/troubleshoot/cli/root.go @@ -44,6 +44,18 @@ If no arguments are provided, specs are automatically loaded from the cluster by RunE: func(cmd *cobra.Command, args []string) error { v := viper.GetViper() + // If there are no locations to load specs from passed in the cli args, we should + // load them from the cluster by setting "load-cluster-specs=true". If the caller + // provided "--load-cluster-specs" cli option, we should respect it. + if len(args) == 0 { + // Check if --load-cluster-specs was set by the cli caller to avoid overriding it + flg := cmd.Flags().Lookup("load-cluster-specs") + if flg != nil && !flg.Changed { + // Load specs from the cluster if no spec(s) is(are) provided in the cli args + v.Set("load-cluster-specs", true) + } + } + closer, err := traces.ConfigureTracing("support-bundle") if err != nil { // Do not fail running support-bundle if tracing fails @@ -77,7 +89,7 @@ If no arguments are provided, specs are automatically loaded from the cluster by cmd.Flags().Bool("interactive", true, "enable/disable interactive mode") cmd.Flags().Bool("collect-without-permissions", true, "always generate a support bundle, even if it some require additional permissions") cmd.Flags().StringSliceP("selector", "l", []string{"troubleshoot.sh/kind=support-bundle"}, "selector to filter on for loading additional support bundle specs found in secrets within the cluster") - cmd.Flags().Bool("load-cluster-specs", false, "enable/disable loading additional troubleshoot specs found within the cluster. This is the default behavior if no spec is provided as an argument") + cmd.Flags().Bool("load-cluster-specs", false, "enable/disable loading additional troubleshoot specs found within the cluster. Do not load by default unless no specs are provided in the cli args") cmd.Flags().String("since-time", "", "force pod logs collectors to return logs after a specific date (RFC3339)") cmd.Flags().String("since", "", "force pod logs collectors to return logs newer than a relative duration like 5s, 2m, or 3h.") cmd.Flags().StringP("output", "o", "", "specify the output file path for the support bundle") diff --git a/cmd/troubleshoot/cli/run.go b/cmd/troubleshoot/cli/run.go index 89cfef523..335f3320f 100644 --- a/cmd/troubleshoot/cli/run.go +++ b/cmd/troubleshoot/cli/run.go @@ -292,24 +292,9 @@ func loadSpecs(ctx context.Context, args []string, client kubernetes.Interface) err error ) - if len(args) < 1 { - fmt.Println("\r\033[36mNo specs provided, attempting to load from cluster...\033[m") - kinds, err = specs.LoadFromCluster(ctx, client, vp.GetStringSlice("selector"), vp.GetString("namespace")) - if err != nil { - return nil, nil, errors.Wrap(err, "failed to load specs from cluster, and no specs were provided as arguments") - } - if len(redactors) > 0 { - additionalKinds, err := specs.LoadFromCLIArgs(ctx, client, allArgs, vp) - if err != nil { - return nil, nil, errors.Wrap(err, "failed to load redactors from CLI args") - } - kinds.RedactorsV1Beta2 = append(kinds.RedactorsV1Beta2, additionalKinds.RedactorsV1Beta2...) - } - } else { - kinds, err = specs.LoadFromCLIArgs(ctx, client, allArgs, vp) - if err != nil { - return nil, nil, errors.Wrap(err, "failed to load specs from CLI args") - } + kinds, err = specs.LoadFromCLIArgs(ctx, client, allArgs, vp) + if err != nil { + return nil, nil, errors.Wrap(err, "failed to load specs from CLI args") } // Load additional specs from support bundle URIs @@ -326,7 +311,10 @@ func loadSpecs(ctx context.Context, args []string, client kubernetes.Interface) if len(kinds.CollectorsV1Beta2) == 0 && len(kinds.HostCollectorsV1Beta2) == 0 && len(kinds.SupportBundlesV1Beta2) == 0 { - return nil, nil, types.NewExitCodeError(constants.EXIT_CODE_CATCH_ALL, errors.Wrap(err, "no collectors specified to run. Use --debug and/or -v=2 to see more information")) + return nil, nil, types.NewExitCodeError( + constants.EXIT_CODE_CATCH_ALL, + errors.New("no collectors specified to run. Use --debug and/or -v=2 to see more information"), + ) } // Merge specs diff --git a/internal/specs/specs.go b/internal/specs/specs.go index 2f442e355..cfb0060f0 100644 --- a/internal/specs/specs.go +++ b/internal/specs/specs.go @@ -268,6 +268,8 @@ func downloadFromHttpURL(ctx context.Context, url string, headers map[string]str // to list & read secrets and configmaps from all namespaces, we will fallback to trying each // namespace individually, and eventually default to the configured kubeconfig namespace. func LoadFromCluster(ctx context.Context, client kubernetes.Interface, selectors []string, ns string) (*loader.TroubleshootKinds, error) { + klog.V(1).Infof("Load troubleshoot specs from the cluster using selectors: %v", selectors) + if reflect.DeepEqual(selectors, []string{"troubleshoot.sh/kind=support-bundle"}) { // Its the default selector so we append troubleshoot.io/kind=support-bundle to it due to backwards compatibility selectors = append(selectors, "troubleshoot.io/kind=support-bundle") diff --git a/pkg/loader/loader.go b/pkg/loader/loader.go index e139835df..872b78278 100644 --- a/pkg/loader/loader.go +++ b/pkg/loader/loader.go @@ -83,14 +83,18 @@ type TroubleshootKinds struct { } func (kinds *TroubleshootKinds) IsEmpty() bool { - return len(kinds.AnalyzersV1Beta2) == 0 && - len(kinds.CollectorsV1Beta2) == 0 && - len(kinds.HostCollectorsV1Beta2) == 0 && - len(kinds.HostPreflightsV1Beta2) == 0 && - len(kinds.PreflightsV1Beta2) == 0 && - len(kinds.RedactorsV1Beta2) == 0 && - len(kinds.RemoteCollectorsV1Beta2) == 0 && - len(kinds.SupportBundlesV1Beta2) == 0 + return kinds.Len() == 0 +} + +func (kinds *TroubleshootKinds) Len() int { + return len(kinds.AnalyzersV1Beta2) + + len(kinds.CollectorsV1Beta2) + + len(kinds.HostCollectorsV1Beta2) + + len(kinds.HostPreflightsV1Beta2) + + len(kinds.PreflightsV1Beta2) + + len(kinds.RedactorsV1Beta2) + + len(kinds.RemoteCollectorsV1Beta2) + + len(kinds.SupportBundlesV1Beta2) } func (kinds *TroubleshootKinds) Add(other *TroubleshootKinds) { @@ -200,7 +204,7 @@ func (l *specLoader) loadFromStrings(rawSpecs ...string) (*TroubleshootKinds, er // If it's not a configmap or secret, just append it to the splitdocs splitdocs = append(splitdocs, rawDoc) } else { - klog.V(1).Infof("Skip loading %q kind", parsed.Kind) + klog.V(2).Infof("Skip loading %q kind", parsed.Kind) } } @@ -254,11 +258,7 @@ func (l *specLoader) loadFromSplitDocs(splitdocs []string) (*TroubleshootKinds, } } - if kinds.IsEmpty() { - klog.V(1).Info("No troubleshoot specs were loaded") - } else { - klog.V(1).Info("Loaded troubleshoot specs successfully") - } + klog.V(2).Infof("Loaded %d troubleshoot specs successfully", kinds.Len()) return kinds, nil } diff --git a/pkg/types/types.go b/pkg/types/types.go index d710362a7..47fa1b5ce 100644 --- a/pkg/types/types.go +++ b/pkg/types/types.go @@ -20,6 +20,7 @@ type ExitError interface { type ExitCodeError struct { Msg string Code int + Err error } type ExitCodeWarning struct { @@ -30,6 +31,10 @@ func (e *ExitCodeError) Error() string { return e.Msg } +func (e *ExitCodeError) Unwrap() error { + return e.Err +} + func (e *ExitCodeError) ExitStatus() int { return e.Code } @@ -39,7 +44,7 @@ func NewExitCodeError(exitCode int, theErr error) *ExitCodeError { if theErr != nil { useErr = theErr.Error() } - return &ExitCodeError{Msg: useErr, Code: exitCode} + return &ExitCodeError{Msg: useErr, Code: exitCode, Err: theErr} } func NewExitCodeWarning(theErrMsg string) *ExitCodeWarning { From ffa1c040e2520d26d4cded71b0e3a058f442145e Mon Sep 17 00:00:00 2001 From: Gerard Nguyen Date: Mon, 14 Oct 2024 14:36:42 +1100 Subject: [PATCH 08/25] fix: [sc-111255] CRD analyzer outcomes has no Warn field (#1647) add warn field to CRD analyzer --- pkg/analyze/crd.go | 6 ++- pkg/analyze/crd_test.go | 101 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+), 1 deletion(-) create mode 100644 pkg/analyze/crd_test.go diff --git a/pkg/analyze/crd.go b/pkg/analyze/crd.go index 980ebe505..ea708bbcc 100644 --- a/pkg/analyze/crd.go +++ b/pkg/analyze/crd.go @@ -66,11 +66,15 @@ func (a *AnalyzeCustomResourceDefinition) analyzeCustomResourceDefinition(analyz } } - result.IsFail = true for _, outcome := range analyzer.Outcomes { if outcome.Fail != nil { + result.IsFail = true result.Message = outcome.Fail.Message result.URI = outcome.Fail.URI + } else if outcome.Warn != nil { + result.IsWarn = true + result.Message = outcome.Warn.Message + result.URI = outcome.Warn.URI } } diff --git a/pkg/analyze/crd_test.go b/pkg/analyze/crd_test.go new file mode 100644 index 000000000..11a7587e4 --- /dev/null +++ b/pkg/analyze/crd_test.go @@ -0,0 +1,101 @@ +package analyzer + +import ( + "encoding/json" + "testing" + + troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" + "github.com/stretchr/testify/assert" + "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestAnalyzeCustomResourceDefinition(t *testing.T) { + getFile := func(_ string) ([]byte, error) { + crdsList := v1beta1.CustomResourceDefinitionList{ + Items: []v1beta1.CustomResourceDefinition{ + {ObjectMeta: metav1.ObjectMeta{Name: "servicemonitors.monitoring.coreos.com"}}, + {ObjectMeta: metav1.ObjectMeta{Name: "probes.monitoring.coreos.com"}}, + }, + } + return json.Marshal(crdsList) + } + + tests := []struct { + name string + analyzer *troubleshootv1beta2.CustomResourceDefinition + expectedResult *AnalyzeResult + }{ + { + name: "CRD exists and pass", + analyzer: &troubleshootv1beta2.CustomResourceDefinition{ + CustomResourceDefinitionName: "servicemonitors.monitoring.coreos.com", + Outcomes: []*troubleshootv1beta2.Outcome{ + { + Pass: &troubleshootv1beta2.SingleOutcome{ + Message: "The ServiceMonitor CRD is installed and available.", + }, + }, + }, + }, + expectedResult: &AnalyzeResult{ + Title: "Custom resource definition servicemonitors.monitoring.coreos.com", + IconKey: "kubernetes_custom_resource_definition", + IconURI: "https://troubleshoot.sh/images/analyzer-icons/custom-resource-definition.svg?w=13&h=16", + IsPass: true, + Message: "The ServiceMonitor CRD is installed and available.", + }, + }, + { + name: "CRD not exists and warn", + analyzer: &troubleshootv1beta2.CustomResourceDefinition{ + CustomResourceDefinitionName: "podmonitors.monitoring.coreos.com", + Outcomes: []*troubleshootv1beta2.Outcome{ + { + Warn: &troubleshootv1beta2.SingleOutcome{ + Message: "The Prometheus PodMonitor CRD was not found in the cluster.", + }, + }, + }, + }, + expectedResult: &AnalyzeResult{ + Title: "Custom resource definition podmonitors.monitoring.coreos.com", + IconKey: "kubernetes_custom_resource_definition", + IconURI: "https://troubleshoot.sh/images/analyzer-icons/custom-resource-definition.svg?w=13&h=16", + IsWarn: true, + Message: "The Prometheus PodMonitor CRD was not found in the cluster.", + }, + }, + { + name: "CRD not exists and fail", + analyzer: &troubleshootv1beta2.CustomResourceDefinition{ + CustomResourceDefinitionName: "backupstoragelocations.velero.io", + Outcomes: []*troubleshootv1beta2.Outcome{ + { + Fail: &troubleshootv1beta2.SingleOutcome{ + Message: "The BackupStorageLocation CRD was not found in the cluster.", + }, + }, + }, + }, + expectedResult: &AnalyzeResult{ + Title: "Custom resource definition backupstoragelocations.velero.io", + IconKey: "kubernetes_custom_resource_definition", + IconURI: "https://troubleshoot.sh/images/analyzer-icons/custom-resource-definition.svg?w=13&h=16", + IsFail: true, + Message: "The BackupStorageLocation CRD was not found in the cluster.", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &AnalyzeCustomResourceDefinition{ + analyzer: tt.analyzer, + } + result, err := a.analyzeCustomResourceDefinition(tt.analyzer, getFile) + assert.NoError(t, err) + assert.Equal(t, tt.expectedResult, result) + }) + } +} From 781853251d9ac9f15deddbf3b2a4c5275a4a12e9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 15 Oct 2024 00:23:02 +0000 Subject: [PATCH 09/25] chore(deps): bump the security group with 3 updates (#1649) Bumps the security group with 3 updates: [go.opentelemetry.io/otel](https://github.com/open-telemetry/opentelemetry-go), [go.opentelemetry.io/otel/sdk](https://github.com/open-telemetry/opentelemetry-go) and [helm.sh/helm/v3](https://github.com/helm/helm). Updates `go.opentelemetry.io/otel` from 1.30.0 to 1.31.0 - [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases) - [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md) - [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.30.0...v1.31.0) Updates `go.opentelemetry.io/otel/sdk` from 1.30.0 to 1.31.0 - [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases) - [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md) - [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.30.0...v1.31.0) Updates `helm.sh/helm/v3` from 3.16.1 to 3.16.2 - [Release notes](https://github.com/helm/helm/releases) - [Commits](https://github.com/helm/helm/compare/v3.16.1...v3.16.2) --- updated-dependencies: - dependency-name: go.opentelemetry.io/otel dependency-type: direct:production update-type: version-update:semver-minor dependency-group: security - dependency-name: go.opentelemetry.io/otel/sdk dependency-type: direct:production update-type: version-update:semver-minor dependency-group: security - dependency-name: helm.sh/helm/v3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: security ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 12 ++++++------ go.sum | 24 ++++++++++++------------ 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/go.mod b/go.mod index e88a77f61..409f7ff63 100644 --- a/go.mod +++ b/go.mod @@ -39,8 +39,8 @@ require ( github.com/stretchr/testify v1.9.0 github.com/tj/go-spin v1.1.0 github.com/vmware-tanzu/velero v1.14.1 - go.opentelemetry.io/otel v1.30.0 - go.opentelemetry.io/otel/sdk v1.30.0 + go.opentelemetry.io/otel v1.31.0 + go.opentelemetry.io/otel/sdk v1.31.0 golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 golang.org/x/mod v0.21.0 golang.org/x/sync v0.8.0 @@ -122,15 +122,15 @@ require ( github.com/xeipuuv/gojsonschema v1.2.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect - go.opentelemetry.io/otel/metric v1.30.0 // indirect - go.opentelemetry.io/otel/trace v1.30.0 // indirect + go.opentelemetry.io/otel/metric v1.31.0 // indirect + go.opentelemetry.io/otel/trace v1.31.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/tools v0.22.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect k8s.io/component-base v0.31.1 // indirect - k8s.io/kubectl v0.31.0 // indirect + k8s.io/kubectl v0.31.1 // indirect ) require ( @@ -252,7 +252,7 @@ require ( gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - helm.sh/helm/v3 v3.16.1 + helm.sh/helm/v3 v3.16.2 k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect k8s.io/kubelet v0.31.1 k8s.io/metrics v0.31.1 diff --git a/go.sum b/go.sum index db360a3d0..588d3c22d 100644 --- a/go.sum +++ b/go.sum @@ -925,8 +925,8 @@ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.5 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0/go.mod h1:azvtTADFQJA8mX80jIH/akaE7h+dbm/sVuaHqN13w74= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg= -go.opentelemetry.io/otel v1.30.0 h1:F2t8sK4qf1fAmY9ua4ohFS/K+FUuOPemHUIXHtktrts= -go.opentelemetry.io/otel v1.30.0/go.mod h1:tFw4Br9b7fOS+uEao81PJjVMjW/5fvNCbpsDIXqP0pc= +go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY= +go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0 h1:jd0+5t/YynESZqsSyPz+7PAFdEop0dlN0+PkyHYo8oI= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0/go.mod h1:U707O40ee1FpQGyhvqnzmCJm1Wh6OX6GGBVn0E6Uyyk= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v0.44.0 h1:bflGWrfYyuulcdxf14V6n9+CoQcu5SAAdHmDPAJnlps= @@ -943,14 +943,14 @@ go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v0.44.0 h1:dEZWPjVN22urgY go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v0.44.0/go.mod h1:sTt30Evb7hJB/gEk27qLb1+l9n4Tb8HvHkR0Wx3S6CU= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.21.0 h1:VhlEQAPp9R1ktYfrPk5SOryw1e9LDDTZCbIPFrho0ec= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.21.0/go.mod h1:kB3ufRbfU+CQ4MlUcqtW8Z7YEOBeK2DJ6CmR5rYYF3E= -go.opentelemetry.io/otel/metric v1.30.0 h1:4xNulvn9gjzo4hjg+wzIKG7iNFEaBMX00Qd4QIZs7+w= -go.opentelemetry.io/otel/metric v1.30.0/go.mod h1:aXTfST94tswhWEb+5QjlSqG+cZlmyXy/u8jFpor3WqQ= -go.opentelemetry.io/otel/sdk v1.30.0 h1:cHdik6irO49R5IysVhdn8oaiR9m8XluDaJAs4DfOrYE= -go.opentelemetry.io/otel/sdk v1.30.0/go.mod h1:p14X4Ok8S+sygzblytT1nqG98QG2KYKv++HE0LY/mhg= +go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE= +go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY= +go.opentelemetry.io/otel/sdk v1.31.0 h1:xLY3abVHYZ5HSfOg3l2E5LUj2Cwva5Y7yGxnSW9H5Gk= +go.opentelemetry.io/otel/sdk v1.31.0/go.mod h1:TfRbMdhvxIIr/B2N2LQW2S5v9m3gOQ/08KsbbO5BPT0= go.opentelemetry.io/otel/sdk/metric v1.21.0 h1:smhI5oD714d6jHE6Tie36fPx4WDFIg+Y6RfAY4ICcR0= go.opentelemetry.io/otel/sdk/metric v1.21.0/go.mod h1:FJ8RAsoPGv/wYMgBdUJXOm+6pzFY3YdljnXtv1SBE8Q= -go.opentelemetry.io/otel/trace v1.30.0 h1:7UBkkYzeg3C7kQX8VAidWh2biiQbtAKjyIML8dQ9wmc= -go.opentelemetry.io/otel/trace v1.30.0/go.mod h1:5EyKqTzzmyqB9bwtCCq6pDLktPK6fmGf/Dph+8VI02o= +go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys= +go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= @@ -1527,8 +1527,8 @@ gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= -helm.sh/helm/v3 v3.16.1 h1:cER6tI/8PgUAsaJaQCVBUg3VI9KN4oVaZJgY60RIc0c= -helm.sh/helm/v3 v3.16.1/go.mod h1:r+xBHHP20qJeEqtvBXMf7W35QDJnzY/eiEBzt+TfHps= +helm.sh/helm/v3 v3.16.2 h1:Y9v7ry+ubQmi+cb5zw1Llx8OKHU9Hk9NQ/+P+LGBe2o= +helm.sh/helm/v3 v3.16.2/go.mod h1:SyTXgKBjNqi2NPsHCW5dDAsHqvGIu0kdNYNH9gQaw70= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= @@ -1554,8 +1554,8 @@ k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag= k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98= -k8s.io/kubectl v0.31.0 h1:kANwAAPVY02r4U4jARP/C+Q1sssCcN/1p9Nk+7BQKVg= -k8s.io/kubectl v0.31.0/go.mod h1:pB47hhFypGsaHAPjlwrNbvhXgmuAr01ZBvAIIUaI8d4= +k8s.io/kubectl v0.31.1 h1:ih4JQJHxsEggFqDJEHSOdJ69ZxZftgeZvYo7M/cpp24= +k8s.io/kubectl v0.31.1/go.mod h1:aNuQoR43W6MLAtXQ/Bu4GDmoHlbhHKuyD49lmTC8eJM= k8s.io/kubelet v0.31.1 h1:aAxwVxGzbbMKKk/FnSjvkN52K3LdHhjhzmYcyGBuE0c= k8s.io/kubelet v0.31.1/go.mod h1:8ZbexYHqUO946gXEfFmnMZiK2UKRGhk7LlGvJ71p2Ig= k8s.io/metrics v0.31.1 h1:h4I4dakgh/zKflWYAOQhwf0EXaqy8LxAIyE/GBvxqRc= From 78ee833f459b529e90c1b2410b6159ec1ce9797d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 15 Oct 2024 00:23:43 +0000 Subject: [PATCH 10/25] chore(deps): bump helm.sh/helm/v3 from 3.16.1 to 3.16.2 in /examples/sdk/helm-template in the security group (#1650) chore(deps): bump helm.sh/helm/v3 Bumps the security group in /examples/sdk/helm-template with 1 update: [helm.sh/helm/v3](https://github.com/helm/helm). Updates `helm.sh/helm/v3` from 3.16.1 to 3.16.2 - [Release notes](https://github.com/helm/helm/releases) - [Commits](https://github.com/helm/helm/compare/v3.16.1...v3.16.2) --- updated-dependencies: - dependency-name: helm.sh/helm/v3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: security ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- examples/sdk/helm-template/go.mod | 20 +++++++------- examples/sdk/helm-template/go.sum | 44 +++++++++++++++---------------- 2 files changed, 32 insertions(+), 32 deletions(-) diff --git a/examples/sdk/helm-template/go.mod b/examples/sdk/helm-template/go.mod index e8075427b..f2e85f871 100644 --- a/examples/sdk/helm-template/go.mod +++ b/examples/sdk/helm-template/go.mod @@ -10,7 +10,7 @@ replace github.com/replicatedhq/troubleshoot v0.0.0 => ../../../ require ( github.com/replicatedhq/troubleshoot v0.0.0 - helm.sh/helm/v3 v3.16.1 + helm.sh/helm/v3 v3.16.2 sigs.k8s.io/yaml v1.4.0 ) @@ -51,21 +51,21 @@ require ( github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect - golang.org/x/crypto v0.27.0 // indirect - golang.org/x/net v0.29.0 // indirect + golang.org/x/crypto v0.28.0 // indirect + golang.org/x/net v0.30.0 // indirect golang.org/x/oauth2 v0.22.0 // indirect - golang.org/x/sys v0.25.0 // indirect - golang.org/x/term v0.24.0 // indirect - golang.org/x/text v0.18.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/term v0.25.0 // indirect + golang.org/x/text v0.19.0 // indirect golang.org/x/time v0.5.0 // indirect google.golang.org/protobuf v1.34.2 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/api v0.31.0 // indirect - k8s.io/apiextensions-apiserver v0.31.0 // indirect - k8s.io/apimachinery v0.31.0 // indirect - k8s.io/client-go v0.31.0 // indirect + k8s.io/api v0.31.1 // indirect + k8s.io/apiextensions-apiserver v0.31.1 // indirect + k8s.io/apimachinery v0.31.1 // indirect + k8s.io/client-go v0.31.1 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // indirect diff --git a/examples/sdk/helm-template/go.sum b/examples/sdk/helm-template/go.sum index bcdf4affb..5ee2394ab 100644 --- a/examples/sdk/helm-template/go.sum +++ b/examples/sdk/helm-template/go.sum @@ -108,16 +108,16 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= -golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= -golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA= golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -126,22 +126,22 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= -golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM= -golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= -golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= +golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -160,16 +160,16 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -helm.sh/helm/v3 v3.16.1 h1:cER6tI/8PgUAsaJaQCVBUg3VI9KN4oVaZJgY60RIc0c= -helm.sh/helm/v3 v3.16.1/go.mod h1:r+xBHHP20qJeEqtvBXMf7W35QDJnzY/eiEBzt+TfHps= -k8s.io/api v0.31.0 h1:b9LiSjR2ym/SzTOlfMHm1tr7/21aD7fSkqgD/CVJBCo= -k8s.io/api v0.31.0/go.mod h1:0YiFF+JfFxMM6+1hQei8FY8M7s1Mth+z/q7eF1aJkTE= -k8s.io/apiextensions-apiserver v0.31.0 h1:fZgCVhGwsclj3qCw1buVXCV6khjRzKC5eCFt24kyLSk= -k8s.io/apiextensions-apiserver v0.31.0/go.mod h1:b9aMDEYaEe5sdK+1T0KU78ApR/5ZVp4i56VacZYEHxk= -k8s.io/apimachinery v0.31.0 h1:m9jOiSr3FoSSL5WO9bjm1n6B9KROYYgNZOb4tyZ1lBc= -k8s.io/apimachinery v0.31.0/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= -k8s.io/client-go v0.31.0 h1:QqEJzNjbN2Yv1H79SsS+SWnXkBgVu4Pj3CJQgbx0gI8= -k8s.io/client-go v0.31.0/go.mod h1:Y9wvC76g4fLjmU0BA+rV+h2cncoadjvjjkkIGoTLcGU= +helm.sh/helm/v3 v3.16.2 h1:Y9v7ry+ubQmi+cb5zw1Llx8OKHU9Hk9NQ/+P+LGBe2o= +helm.sh/helm/v3 v3.16.2/go.mod h1:SyTXgKBjNqi2NPsHCW5dDAsHqvGIu0kdNYNH9gQaw70= +k8s.io/api v0.31.1 h1:Xe1hX/fPW3PXYYv8BlozYqw63ytA92snr96zMW9gWTU= +k8s.io/api v0.31.1/go.mod h1:sbN1g6eY6XVLeqNsZGLnI5FwVseTrZX7Fv3O26rhAaI= +k8s.io/apiextensions-apiserver v0.31.1 h1:L+hwULvXx+nvTYX/MKM3kKMZyei+UiSXQWciX/N6E40= +k8s.io/apiextensions-apiserver v0.31.1/go.mod h1:tWMPR3sgW+jsl2xm9v7lAyRF1rYEK71i9G5dRtkknoQ= +k8s.io/apimachinery v0.31.1 h1:mhcUBbj7KUjaVhyXILglcVjuS4nYXiwC+KKFBgIVy7U= +k8s.io/apimachinery v0.31.1/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= +k8s.io/client-go v0.31.1 h1:f0ugtWSbWpxHR7sjVpQwuvw9a3ZKLXX0u0itkFXufb0= +k8s.io/client-go v0.31.1/go.mod h1:sKI8871MJN2OyeqRlmA4W4KM9KBdBUpDLu/43eGemCg= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag= From 77fd7da315f886dd065ae9c1e4451075e1664850 Mon Sep 17 00:00:00 2001 From: Dexter Yan Date: Wed, 16 Oct 2024 13:27:14 +1300 Subject: [PATCH 11/25] fix(make): make sure github action can check schemas change (#1651) --- .github/workflows/build-test-deploy.yaml | 7 +++++-- Makefile | 4 ++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-test-deploy.yaml b/.github/workflows/build-test-deploy.yaml index 0849d49a8..d35380708 100644 --- a/.github/workflows/build-test-deploy.yaml +++ b/.github/workflows/build-test-deploy.yaml @@ -81,7 +81,10 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 - - run: make check-schemas + path: github.com/replicatedhq/troubleshoot + - run: | + cd github.com/replicatedhq/troubleshoot + make check-schemas compile-preflight: runs-on: ubuntu-latest @@ -388,4 +391,4 @@ jobs: # if the validate-pr-tests job was successful, this job will succeed - name: succeed if validate-pr-tests job succeeded if: needs.validate-pr-tests.result == 'success' - run: echo "Validation succeeded" \ No newline at end of file + run: echo "Validation succeeded" diff --git a/Makefile b/Makefile index 02593d246..391daa8ad 100644 --- a/Makefile +++ b/Makefile @@ -133,7 +133,7 @@ generate: controller-gen client-gen $(CONTROLLER_GEN) \ object:headerFile=./hack/boilerplate.go.txt paths=./pkg/apis/... $(CLIENT_GEN) \ - --output-base=./../../../ \ + --output-base=$$(pwd)/../../../ \ --output-package=github.com/replicatedhq/troubleshoot/pkg/client \ --clientset-name troubleshootclientset \ --input-base github.com/replicatedhq/troubleshoot/pkg/apis \ @@ -147,7 +147,7 @@ openapischema: controller-gen controller-gen crd +output:dir=./config/crds paths=./pkg/apis/troubleshoot/v1beta2 check-schemas: generate schemas - @if [ -n "$(shell git status --short)" ]; then \ + @if [ -n "$$(git status --short)" ]; then \ echo -e "\033[31mThe git repo is dirty :( Ensure all generated files are committed e.g CRD schema files\033[0;m"; \ git status --short; \ exit 1; \ From 289102f16ddfab5df50ba3c93145b6e4e93269ee Mon Sep 17 00:00:00 2001 From: Gerard Nguyen Date: Fri, 18 Oct 2024 15:58:32 +1100 Subject: [PATCH 12/25] bug: fix nil check in host collector filter (#1653) * add nil check in filter host collector --- pkg/supportbundle/collect.go | 10 +++---- pkg/supportbundle/collect_test.go | 45 +++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 5 deletions(-) create mode 100644 pkg/supportbundle/collect_test.go diff --git a/pkg/supportbundle/collect.go b/pkg/supportbundle/collect.go index 6cff37701..303b68bcc 100644 --- a/pkg/supportbundle/collect.go +++ b/pkg/supportbundle/collect.go @@ -290,7 +290,6 @@ func collectHost(ctx context.Context, filteredCollectors []FilteredCollector, op if err != nil { span.SetStatus(codes.Error, err.Error()) opts.ProgressChan <- fmt.Sprintf("[%s] Error: %v", collector.Title(), err) - return errors.Wrap(err, "failed to run host collector") } // Send progress event: completed successfully @@ -332,13 +331,14 @@ func filterHostCollectors(ctx context.Context, collectSpecs []*troubleshootv1bet for _, desiredCollector := range collectSpecs { collector, ok := collect.GetHostCollector(desiredCollector, bundlePath) - _, span := otel.Tracer(constants.LIB_TRACER_NAME).Start(ctx, collector.Title()) - span.SetAttributes(attribute.String("type", reflect.TypeOf(collector).String())) - if !ok { - return nil, collect.ErrHostCollectorNotFound + opts.ProgressChan <- "Host collector not found" + continue } + _, span := otel.Tracer(constants.LIB_TRACER_NAME).Start(ctx, collector.Title()) + span.SetAttributes(attribute.String("type", reflect.TypeOf(collector).String())) + isExcluded, _ := collector.IsExcluded() if isExcluded { opts.ProgressChan <- fmt.Sprintf("[%s] Excluding host collector", collector.Title()) diff --git a/pkg/supportbundle/collect_test.go b/pkg/supportbundle/collect_test.go new file mode 100644 index 000000000..0dfda3e68 --- /dev/null +++ b/pkg/supportbundle/collect_test.go @@ -0,0 +1,45 @@ +package supportbundle + +import ( + "context" + "testing" + + v1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/trace/noop" +) + +func Test_filterHostCollectors(t *testing.T) { + otel.SetTracerProvider(noop.NewTracerProvider()) + + testCases := []struct { + name string + collectSpecs []*v1beta2.HostCollect + bundlePath string + opts SupportBundleCreateOpts + expectedResult []FilteredCollector + expectedError error + }{ + { + name: "nil host collectors spec", + collectSpecs: []*v1beta2.HostCollect{}, + bundlePath: "/tmp", + opts: SupportBundleCreateOpts{ + ProgressChan: make(chan interface{}, 10), + }, + expectedResult: []FilteredCollector{}, + expectedError: nil, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + filtered, err := filterHostCollectors(context.TODO(), tc.collectSpecs, tc.bundlePath, tc.opts) + if err != tc.expectedError { + t.Fatalf("expected error %v, got %v", tc.expectedError, err) + } + if len(filtered) != len(tc.expectedResult) { + t.Fatalf("expected %d filtered collectors, got %d", len(tc.expectedResult), len(filtered)) + } + }) + } +} From 9c24ab6067dbc16944f530f7d1d2b460548376a5 Mon Sep 17 00:00:00 2001 From: Evans Mungai Date: Mon, 21 Oct 2024 16:35:36 -0500 Subject: [PATCH 13/25] chore: Remove preempted deprecation warnings (#1655) Signed-off-by: Evans Mungai --- go.mod | 2 +- pkg/supportbundle/load.go | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 409f7ff63..c504a25cf 100644 --- a/go.mod +++ b/go.mod @@ -41,6 +41,7 @@ require ( github.com/vmware-tanzu/velero v1.14.1 go.opentelemetry.io/otel v1.31.0 go.opentelemetry.io/otel/sdk v1.31.0 + go.opentelemetry.io/otel/trace v1.31.0 golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 golang.org/x/mod v0.21.0 golang.org/x/sync v0.8.0 @@ -123,7 +124,6 @@ require ( go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect go.opentelemetry.io/otel/metric v1.31.0 // indirect - go.opentelemetry.io/otel/trace v1.31.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/tools v0.22.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 // indirect diff --git a/pkg/supportbundle/load.go b/pkg/supportbundle/load.go index bbbaf275f..a5152cf3b 100644 --- a/pkg/supportbundle/load.go +++ b/pkg/supportbundle/load.go @@ -38,7 +38,7 @@ func GetSupportBundleFromURI(bundleURI string) (*troubleshootv1beta2.SupportBund } // ParseSupportBundle parses a support bundle from a byte array into a SupportBundle object -// Deprecated: use loader.LoadSpecs instead +// We will deprecate this in favour of use loader.LoadSpecs once the new API is stable func ParseSupportBundle(doc []byte, followURI bool) (*troubleshootv1beta2.SupportBundle, error) { doc, err := docrewrite.ConvertToV1Beta2(doc) if err != nil { @@ -102,13 +102,13 @@ func ParseSupportBundle(doc []byte, followURI bool) (*troubleshootv1beta2.Suppor } // ParseSupportBundle parses a support bundle from a byte array into a SupportBundle object -// Deprecated: use loader.LoadSpecs instead +// We will deprecate this in favour of use loader.LoadSpecs once the new API is stable func ParseSupportBundleFromDoc(doc []byte) (*troubleshootv1beta2.SupportBundle, error) { return ParseSupportBundle(doc, true) } // GetRedactorFromURI parses a redactor from a URI into a Redactor object -// Deprecated: use loader.LoadSpecs instead +// We will deprecate this in favour of use loader.LoadSpecs once the new API is stable func GetRedactorFromURI(redactorURI string) (*troubleshootv1beta2.Redactor, error) { redactorContent, err := LoadRedactorSpec(redactorURI) if err != nil { @@ -127,7 +127,7 @@ func GetRedactorFromURI(redactorURI string) (*troubleshootv1beta2.Redactor, erro } // GetRedactorsFromURIs parses redactors from a URIs Redactor objects -// Deprecated: use loader.LoadSpecs instead +// We will deprecate this in favour of use loader.LoadSpecs once the new API is stable func GetRedactorsFromURIs(redactorURIs []string) ([]*troubleshootv1beta2.Redact, error) { redactors := []*troubleshootv1beta2.Redact{} for _, redactor := range redactorURIs { @@ -263,7 +263,7 @@ func loadSpecFromURL(arg string) ([]byte, error) { } // ParseRedactorsFromDocs parses a slice of YAML docs and returns a slice of Redactors -// Deprecated: use loader.LoadSpecs instead +// We will deprecate this in favour of use loader.LoadSpecs once the new API is stable func ParseRedactorsFromDocs(docs []string) ([]*troubleshootv1beta2.Redact, error) { var redactors []*troubleshootv1beta2.Redact From b88bc8ddf766b237b45aaa734d38294ec8f3b3cb Mon Sep 17 00:00:00 2001 From: Diamon Wiggins <38189728+diamonwiggins@users.noreply.github.com> Date: Mon, 21 Oct 2024 17:45:50 -0400 Subject: [PATCH 14/25] Refactor Multi Node Analyzers (#1646) * initial refactor of host os analyzer * refactor remote collect analysis --------- Signed-off-by: Evans Mungai Co-authored-by: Gerard Nguyen Co-authored-by: Evans Mungai --- pkg/analyze/collected_contents.go | 58 +++ pkg/analyze/collected_contents_test.go | 138 +++++++ pkg/analyze/host_analyzer.go | 106 ++++- pkg/analyze/host_analyzer_test.go | 299 ++++++++++++++ pkg/analyze/host_memory.go | 122 ++---- pkg/analyze/host_memory_test.go | 271 ++++++++----- pkg/analyze/host_os_info.go | 228 ++++------- pkg/analyze/host_os_info_test.go | 534 ++++++++++--------------- pkg/collect/host_memory.go | 1 + pkg/supportbundle/collect.go | 91 ++--- pkg/supportbundle/collect_test.go | 45 --- 11 files changed, 1128 insertions(+), 765 deletions(-) create mode 100644 pkg/analyze/collected_contents.go create mode 100644 pkg/analyze/collected_contents_test.go create mode 100644 pkg/analyze/host_analyzer_test.go delete mode 100644 pkg/supportbundle/collect_test.go diff --git a/pkg/analyze/collected_contents.go b/pkg/analyze/collected_contents.go new file mode 100644 index 000000000..e8abe7ee5 --- /dev/null +++ b/pkg/analyze/collected_contents.go @@ -0,0 +1,58 @@ +package analyzer + +import ( + "encoding/json" + "fmt" + + "github.com/pkg/errors" + "github.com/replicatedhq/troubleshoot/pkg/constants" +) + +type collectedContent struct { + NodeName string + Data collectorData +} + +type collectorData interface{} + +type nodeNames struct { + Nodes []string `json:"nodes"` +} + +func retrieveCollectedContents( + getCollectedFileContents func(string) ([]byte, error), + localPath string, remoteNodeBaseDir string, remoteFileName string, +) ([]collectedContent, error) { + var collectedContents []collectedContent + + // Try to retrieve local data first + if contents, err := getCollectedFileContents(localPath); err == nil { + collectedContents = append(collectedContents, collectedContent{NodeName: "", Data: contents}) + // Return immediately if local content is available + return collectedContents, nil + } + + // Local data not available, move to remote collection + nodeListContents, err := getCollectedFileContents(constants.NODE_LIST_FILE) + if err != nil { + return nil, errors.Wrap(err, "failed to get node list") + } + + var nodeNames nodeNames + if err := json.Unmarshal(nodeListContents, &nodeNames); err != nil { + return nil, errors.Wrap(err, "failed to unmarshal node names") + } + + // Collect data for each node + for _, node := range nodeNames.Nodes { + nodeFilePath := fmt.Sprintf("%s/%s/%s", remoteNodeBaseDir, node, remoteFileName) + nodeContents, err := getCollectedFileContents(nodeFilePath) + if err != nil { + return nil, errors.Wrapf(err, "failed to retrieve content for node %s", node) + } + + collectedContents = append(collectedContents, collectedContent{NodeName: node, Data: nodeContents}) + } + + return collectedContents, nil +} diff --git a/pkg/analyze/collected_contents_test.go b/pkg/analyze/collected_contents_test.go new file mode 100644 index 000000000..c7200b4db --- /dev/null +++ b/pkg/analyze/collected_contents_test.go @@ -0,0 +1,138 @@ +package analyzer + +import ( + "encoding/json" + "testing" + + "github.com/replicatedhq/troubleshoot/pkg/constants" + "github.com/replicatedhq/troubleshoot/pkg/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRetrieveCollectedContents(t *testing.T) { + tests := []struct { + name string + getCollectedFileContents func(string) ([]byte, error) // Mock function + localPath string + remoteNodeBaseDir string + remoteFileName string + expectedResult []collectedContent + expectedError string + }{ + { + name: "successfully retrieve local content", + getCollectedFileContents: func(path string) ([]byte, error) { + if path == "localPath" { + return []byte("localContent"), nil + } + return nil, &types.NotFoundError{Name: path} + }, + localPath: "localPath", + remoteNodeBaseDir: "remoteBaseDir", + remoteFileName: "remoteFileName", + expectedResult: []collectedContent{ + { + NodeName: "", + Data: []byte("localContent"), + }, + }, + expectedError: "", + }, + { + name: "local content not found, retrieve remote node content successfully", + getCollectedFileContents: func(path string) ([]byte, error) { + if path == constants.NODE_LIST_FILE { + nodeNames := nodeNames{Nodes: []string{"node1", "node2"}} + return json.Marshal(nodeNames) + } + if path == "remoteBaseDir/node1/remoteFileName" { + return []byte("remoteContent1"), nil + } + if path == "remoteBaseDir/node2/remoteFileName" { + return []byte("remoteContent2"), nil + } + return nil, &types.NotFoundError{Name: path} + }, + localPath: "localPath", + remoteNodeBaseDir: "remoteBaseDir", + remoteFileName: "remoteFileName", + expectedResult: []collectedContent{ + { + NodeName: "node1", + Data: []byte("remoteContent1"), + }, + { + NodeName: "node2", + Data: []byte("remoteContent2"), + }, + }, + expectedError: "", + }, + { + name: "fail to retrieve local content and node list", + getCollectedFileContents: func(path string) ([]byte, error) { + return nil, &types.NotFoundError{Name: path} + }, + localPath: "localPath", + remoteNodeBaseDir: "remoteBaseDir", + remoteFileName: "remoteFileName", + expectedResult: nil, + expectedError: "failed to get node list", + }, + { + name: "fail to retrieve content for one of the nodes", + getCollectedFileContents: func(path string) ([]byte, error) { + if path == constants.NODE_LIST_FILE { + nodeNames := nodeNames{Nodes: []string{"node1", "node2"}} + return json.Marshal(nodeNames) + } + if path == "remoteBaseDir/node1/remoteFileName" { + return []byte("remoteContent1"), nil + } + if path == "remoteBaseDir/node2/remoteFileName" { + return nil, &types.NotFoundError{Name: path} + } + return nil, &types.NotFoundError{Name: path} + }, + localPath: "localPath", + remoteNodeBaseDir: "remoteBaseDir", + remoteFileName: "remoteFileName", + expectedResult: nil, + expectedError: "failed to retrieve content for node node2", + }, + { + name: "fail to unmarshal node list", + getCollectedFileContents: func(path string) ([]byte, error) { + if path == constants.NODE_LIST_FILE { + return []byte("invalidJSON"), nil + } + return nil, &types.NotFoundError{Name: path} + }, + localPath: "localPath", + remoteNodeBaseDir: "remoteBaseDir", + remoteFileName: "remoteFileName", + expectedResult: nil, + expectedError: "failed to unmarshal node names", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + result, err := retrieveCollectedContents( + test.getCollectedFileContents, + test.localPath, + test.remoteNodeBaseDir, + test.remoteFileName, + ) + + if test.expectedError != "" { + require.Error(t, err) + assert.Contains(t, err.Error(), test.expectedError) + } else { + require.NoError(t, err) + assert.Equal(t, test.expectedResult, result) + } + }) + } +} diff --git a/pkg/analyze/host_analyzer.go b/pkg/analyze/host_analyzer.go index cae6a26b3..6edc709d7 100644 --- a/pkg/analyze/host_analyzer.go +++ b/pkg/analyze/host_analyzer.go @@ -1,6 +1,11 @@ package analyzer -import troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" +import ( + "fmt" + + "github.com/pkg/errors" + troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" +) type HostAnalyzer interface { Title() string @@ -83,3 +88,102 @@ func (c *resultCollector) get(title string) []*AnalyzeResult { } return []*AnalyzeResult{{Title: title, IsWarn: true, Message: "no results"}} } + +func analyzeHostCollectorResults(collectedContent []collectedContent, outcomes []*troubleshootv1beta2.Outcome, checkCondition func(string, collectorData) (bool, error), title string) ([]*AnalyzeResult, error) { + var results []*AnalyzeResult + for _, content := range collectedContent { + currentTitle := title + if content.NodeName != "" { + currentTitle = fmt.Sprintf("%s - Node %s", title, content.NodeName) + } + + analyzeResult, err := evaluateOutcomes(outcomes, checkCondition, content.Data, currentTitle) + if err != nil { + return nil, errors.Wrap(err, "failed to evaluate outcomes") + } + if analyzeResult != nil { + results = append(results, analyzeResult...) + } + } + return results, nil +} + +func evaluateOutcomes(outcomes []*troubleshootv1beta2.Outcome, checkCondition func(string, collectorData) (bool, error), data collectorData, title string) ([]*AnalyzeResult, error) { + var results []*AnalyzeResult + + for _, outcome := range outcomes { + result := AnalyzeResult{ + Title: title, + } + + switch { + case outcome.Fail != nil: + if outcome.Fail.When == "" { + result.IsFail = true + result.Message = outcome.Fail.Message + result.URI = outcome.Fail.URI + results = append(results, &result) + return results, nil + } + + isMatch, err := checkCondition(outcome.Fail.When, data) + if err != nil { + return []*AnalyzeResult{&result}, errors.Wrapf(err, "failed to compare %s", outcome.Fail.When) + } + + if isMatch { + result.IsFail = true + result.Message = outcome.Fail.Message + result.URI = outcome.Fail.URI + results = append(results, &result) + return results, nil + } + + case outcome.Warn != nil: + if outcome.Warn.When == "" { + result.IsWarn = true + result.Message = outcome.Warn.Message + result.URI = outcome.Warn.URI + results = append(results, &result) + return results, nil + } + + isMatch, err := checkCondition(outcome.Warn.When, data) + if err != nil { + return []*AnalyzeResult{&result}, errors.Wrapf(err, "failed to compare %s", outcome.Warn.When) + } + + if isMatch { + result.IsWarn = true + result.Message = outcome.Warn.Message + result.URI = outcome.Warn.URI + results = append(results, &result) + return results, nil + } + + case outcome.Pass != nil: + if outcome.Pass.When == "" { + result.IsPass = true + result.Message = outcome.Pass.Message + result.URI = outcome.Pass.URI + results = append(results, &result) + return results, nil + } + + isMatch, err := checkCondition(outcome.Pass.When, data) + if err != nil { + return []*AnalyzeResult{&result}, errors.Wrapf(err, "failed to compare %s", outcome.Pass.When) + } + + if isMatch { + result.IsPass = true + result.Message = outcome.Pass.Message + result.URI = outcome.Pass.URI + results = append(results, &result) + return results, nil + } + } + } + + return nil, nil +} diff --git a/pkg/analyze/host_analyzer_test.go b/pkg/analyze/host_analyzer_test.go new file mode 100644 index 000000000..21eca0f71 --- /dev/null +++ b/pkg/analyze/host_analyzer_test.go @@ -0,0 +1,299 @@ +package analyzer + +import ( + "testing" + + "github.com/pkg/errors" + troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" + "github.com/replicatedhq/troubleshoot/pkg/collect" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestAnalyzeHostCollectorResults(t *testing.T) { + tests := []struct { + name string + outcomes []*troubleshootv1beta2.Outcome + collectedContent []collectedContent + checkCondition func(string, collectorData) (bool, error) + expectResult []*AnalyzeResult + }{ + { + name: "pass if ubuntu >= 00.1.2", + collectedContent: []collectedContent{ + { + NodeName: "node1", + Data: collect.HostOSInfo{ + Name: "myhost", + KernelVersion: "5.4.0-1034-gcp", + PlatformVersion: "00.1.2", + Platform: "ubuntu", + }, + }, + }, + outcomes: []*troubleshootv1beta2.Outcome{ + { + Pass: &troubleshootv1beta2.SingleOutcome{ + When: "ubuntu >= 00.1.2", + Message: "supported distribution matches ubuntu >= 00.1.2", + }, + }, + { + Fail: &troubleshootv1beta2.SingleOutcome{ + Message: "unsupported distribution", + }, + }, + }, + checkCondition: func(when string, data collectorData) (bool, error) { + osInfo := data.(collect.HostOSInfo) + return osInfo.Platform == "ubuntu" && osInfo.PlatformVersion >= "00.1.2", nil + }, + expectResult: []*AnalyzeResult{ + { + Title: "Host OS Info - Node node1", + IsPass: true, + Message: "supported distribution matches ubuntu >= 00.1.2", + }, + }, + }, + { + name: "fail if ubuntu <= 11.04", + collectedContent: []collectedContent{ + { + NodeName: "node1", + Data: collect.HostOSInfo{ + Name: "myhost", + KernelVersion: "5.4.0-1034-gcp", + PlatformVersion: "11.04", + Platform: "ubuntu", + }, + }, + { + NodeName: "node2", + Data: collect.HostOSInfo{ + Name: "myhost", + KernelVersion: "5.4.0-1034-gcp", + PlatformVersion: "11.04", + Platform: "ubuntu", + }, + }, + }, + outcomes: []*troubleshootv1beta2.Outcome{ + { + Fail: &troubleshootv1beta2.SingleOutcome{ + When: "ubuntu <= 11.04", + Message: "unsupported ubuntu version 11.04", + }, + }, + { + Pass: &troubleshootv1beta2.SingleOutcome{ + Message: "supported distribution", + }, + }, + }, + checkCondition: func(when string, data collectorData) (bool, error) { + osInfo := data.(collect.HostOSInfo) + return osInfo.Platform == "ubuntu" && osInfo.PlatformVersion <= "11.04", nil + }, + expectResult: []*AnalyzeResult{ + { + Title: "Host OS Info - Node node1", + IsFail: true, + Message: "unsupported ubuntu version 11.04", + }, + { + Title: "Host OS Info - Node node2", + IsFail: true, + Message: "unsupported ubuntu version 11.04", + }, + }, + }, + { + name: "title does not include node name if empty", + collectedContent: []collectedContent{ + { + NodeName: "", + Data: collect.HostOSInfo{ + Name: "myhost", + KernelVersion: "5.4.0-1034-gcp", + PlatformVersion: "20.04", + Platform: "ubuntu", + }, + }, + }, + outcomes: []*troubleshootv1beta2.Outcome{ + { + Pass: &troubleshootv1beta2.SingleOutcome{ + When: "ubuntu >= 20.04", + Message: "supported distribution matches ubuntu >= 20.04", + }, + }, + { + Fail: &troubleshootv1beta2.SingleOutcome{ + Message: "unsupported distribution", + }, + }, + }, + checkCondition: func(when string, data collectorData) (bool, error) { + osInfo := data.(collect.HostOSInfo) + return osInfo.Platform == "ubuntu" && osInfo.PlatformVersion >= "20.04", nil + }, + expectResult: []*AnalyzeResult{ + { + Title: "Host OS Info", // Ensuring the title does not include node name if it's empty + IsPass: true, + Message: "supported distribution matches ubuntu >= 20.04", + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + // Call the new analyzeHostCollectorResults function with the test data + result, err := analyzeHostCollectorResults(test.collectedContent, test.outcomes, test.checkCondition, "Host OS Info") + require.NoError(t, err) + assert.Equal(t, test.expectResult, result) + }) + } +} + +func TestEvaluateOutcomes(t *testing.T) { + tests := []struct { + name string + outcomes []*troubleshootv1beta2.Outcome + checkCondition func(string, collectorData) (bool, error) + data collectorData + expectedResult []*AnalyzeResult + }{ + { + name: "fail condition matches", + outcomes: []*troubleshootv1beta2.Outcome{ + { + Fail: &troubleshootv1beta2.SingleOutcome{ + When: "failCondition", + Message: "failure condition met", + }, + }, + }, + checkCondition: func(when string, data collectorData) (bool, error) { + // Return true if the condition being checked matches "failCondition" + return when == "failCondition", nil + }, + data: "someData", + expectedResult: []*AnalyzeResult{ + { + Title: "Test Title", + IsFail: true, + Message: "failure condition met", + }, + }, + }, + { + name: "warn condition matches", + outcomes: []*troubleshootv1beta2.Outcome{ + { + Warn: &troubleshootv1beta2.SingleOutcome{ + When: "warnCondition", + Message: "warning condition met", + }, + }, + }, + checkCondition: func(when string, data collectorData) (bool, error) { + // Return true if the condition being checked matches "warnCondition" + return when == "warnCondition", nil + }, + data: "someData", + expectedResult: []*AnalyzeResult{ + { + Title: "Test Title", + IsWarn: true, + Message: "warning condition met", + }, + }, + }, + { + name: "pass condition matches", + outcomes: []*troubleshootv1beta2.Outcome{ + { + Pass: &troubleshootv1beta2.SingleOutcome{ + When: "passCondition", + Message: "pass condition met", + }, + }, + }, + checkCondition: func(when string, data collectorData) (bool, error) { + // Return true if the condition being checked matches "passCondition" + return when == "passCondition", nil + }, + data: "someData", + expectedResult: []*AnalyzeResult{ + { + Title: "Test Title", + IsPass: true, + Message: "pass condition met", + }, + }, + }, + { + name: "no condition matches", + outcomes: []*troubleshootv1beta2.Outcome{ + { + Fail: &troubleshootv1beta2.SingleOutcome{ + When: "failCondition", + Message: "failure condition met", + }, + Warn: &troubleshootv1beta2.SingleOutcome{ + When: "warnCondition", + Message: "warning condition met", + }, + Pass: &troubleshootv1beta2.SingleOutcome{ + When: "passCondition", + Message: "pass condition met", + }, + }, + }, + checkCondition: func(when string, data collectorData) (bool, error) { + // Always return false to simulate no condition matching + return false, nil + }, + data: "someData", + expectedResult: nil, // No condition matches, so we expect no results + }, + { + name: "error in checkCondition", + outcomes: []*troubleshootv1beta2.Outcome{ + { + Fail: &troubleshootv1beta2.SingleOutcome{ + When: "failCondition", + Message: "failure condition met", + }, + }, + }, + checkCondition: func(when string, data collectorData) (bool, error) { + // Simulate an error occurring during condition evaluation + return false, errors.New("mock error") + }, + data: "someData", + expectedResult: []*AnalyzeResult{ + { + Title: "Test Title", + IsFail: false, // Error occurred, so no success flag + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + result, err := evaluateOutcomes(test.outcomes, test.checkCondition, test.data, "Test Title") + + if test.name == "error in checkCondition" { + require.Error(t, err) + } else { + require.NoError(t, err) + assert.Equal(t, test.expectedResult, result) + } + }) + } +} diff --git a/pkg/analyze/host_memory.go b/pkg/analyze/host_memory.go index 8eb5f1566..92ed2f450 100644 --- a/pkg/analyze/host_memory.go +++ b/pkg/analyze/host_memory.go @@ -3,6 +3,7 @@ package analyzer import ( "encoding/json" "fmt" + "reflect" "strings" "github.com/pkg/errors" @@ -26,95 +27,40 @@ func (a *AnalyzeHostMemory) IsExcluded() (bool, error) { func (a *AnalyzeHostMemory) Analyze( getCollectedFileContents func(string) ([]byte, error), findFiles getChildCollectedFileContents, ) ([]*AnalyzeResult, error) { - hostAnalyzer := a.hostAnalyzer - - contents, err := getCollectedFileContents(collect.HostMemoryPath) + result := AnalyzeResult{Title: a.Title()} + + // Use the generic function to collect both local and remote data + collectedContents, err := retrieveCollectedContents( + getCollectedFileContents, + collect.HostMemoryPath, // Local path + collect.NodeInfoBaseDir, // Remote base directory + collect.HostMemoryFileName, // Remote file name + ) if err != nil { - return nil, errors.Wrap(err, "failed to get collected file") - } - - memoryInfo := collect.MemoryInfo{} - if err := json.Unmarshal(contents, &memoryInfo); err != nil { - return nil, errors.Wrap(err, "failed to unmarshal memory info") + return []*AnalyzeResult{&result}, err } - result := AnalyzeResult{ - Title: a.Title(), + results, err := analyzeHostCollectorResults(collectedContents, a.hostAnalyzer.Outcomes, a.CheckCondition, a.Title()) + if err != nil { + return nil, errors.Wrap(err, "failed to analyze OS version") } - for _, outcome := range hostAnalyzer.Outcomes { - - if outcome.Fail != nil { - if outcome.Fail.When == "" { - result.IsFail = true - result.Message = outcome.Fail.Message - result.URI = outcome.Fail.URI - - return []*AnalyzeResult{&result}, nil - } - - isMatch, err := compareHostMemoryConditionalToActual(outcome.Fail.When, memoryInfo.Total) - if err != nil { - return nil, errors.Wrapf(err, "failed to compare %s", outcome.Fail.When) - } - - if isMatch { - result.IsFail = true - result.Message = outcome.Fail.Message - result.URI = outcome.Fail.URI - - return []*AnalyzeResult{&result}, nil - } - } else if outcome.Warn != nil { - if outcome.Warn.When == "" { - result.IsWarn = true - result.Message = outcome.Warn.Message - result.URI = outcome.Warn.URI - - return []*AnalyzeResult{&result}, nil - } - - isMatch, err := compareHostMemoryConditionalToActual(outcome.Warn.When, memoryInfo.Total) - if err != nil { - return nil, errors.Wrapf(err, "failed to compare %s", outcome.Warn.When) - } - - if isMatch { - result.IsWarn = true - result.Message = outcome.Warn.Message - result.URI = outcome.Warn.URI - - return []*AnalyzeResult{&result}, nil - } - } else if outcome.Pass != nil { - if outcome.Pass.When == "" { - result.IsPass = true - result.Message = outcome.Pass.Message - result.URI = outcome.Pass.URI - - return []*AnalyzeResult{&result}, nil - } - - isMatch, err := compareHostMemoryConditionalToActual(outcome.Pass.When, memoryInfo.Total) - if err != nil { - return nil, errors.Wrapf(err, "failed to compare %s", outcome.Pass.When) - } - - if isMatch { - result.IsPass = true - result.Message = outcome.Pass.Message - result.URI = outcome.Pass.URI + return results, nil +} - return []*AnalyzeResult{&result}, nil - } - } +// checkCondition checks the condition of the when clause +func (a *AnalyzeHostMemory) CheckCondition(when string, data collectorData) (bool, error) { + rawData, ok := data.([]byte) + if !ok { + return false, fmt.Errorf("expected data to be []uint8 (raw bytes), got: %v", reflect.TypeOf(data)) } - return []*AnalyzeResult{&result}, nil -} + var memInfo collect.MemoryInfo + if err := json.Unmarshal(rawData, &memInfo); err != nil { + return false, fmt.Errorf("failed to unmarshal data into MemoryInfo: %v", err) + } -func compareHostMemoryConditionalToActual(conditional string, total uint64) (res bool, err error) { - parts := strings.Split(conditional, " ") + parts := strings.Split(when, " ") if len(parts) != 2 { return false, fmt.Errorf("Expected 2 parts in conditional, got %d", len(parts)) } @@ -129,19 +75,23 @@ func compareHostMemoryConditionalToActual(conditional string, total uint64) (res if !ok { return false, fmt.Errorf("could not parse quantity %q", desired) } + if desiredInt < 0 { + return false, fmt.Errorf("desired value must be a positive integer, got %d", desiredInt) + } switch operator { case "<": - return total < uint64(desiredInt), nil + return memInfo.Total < uint64(desiredInt), nil case "<=": - return total <= uint64(desiredInt), nil + return memInfo.Total <= uint64(desiredInt), nil case ">": - return total > uint64(desiredInt), nil + return memInfo.Total > uint64(desiredInt), nil case ">=": - return total >= uint64(desiredInt), nil + return memInfo.Total >= uint64(desiredInt), nil case "=", "==", "===": - return total == uint64(desiredInt), nil + return memInfo.Total == uint64(desiredInt), nil + default: + return false, fmt.Errorf("unsupported operator: %q", operator) } - return false, errors.New("unknown operator") } diff --git a/pkg/analyze/host_memory_test.go b/pkg/analyze/host_memory_test.go index 363c4b738..3bcd09829 100644 --- a/pkg/analyze/host_memory_test.go +++ b/pkg/analyze/host_memory_test.go @@ -2,91 +2,110 @@ package analyzer import ( "encoding/json" + "fmt" "testing" + "github.com/pkg/errors" troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" "github.com/replicatedhq/troubleshoot/pkg/collect" + "github.com/replicatedhq/troubleshoot/pkg/constants" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func Test_doCompareHostMemory(t *testing.T) { +func TestAnalyzeHostMemoryCheckCondition(t *testing.T) { tests := []struct { name string conditional string actual uint64 expected bool + expectErr bool }{ { name: "< 16Gi when actual is 8Gi", conditional: "< 16Gi", - actual: 8 * 1024 * 1024 * 1024, + actual: 8 * 1024 * 1024 * 1024, // 8GiB expected: true, + expectErr: false, }, { name: "< 8Gi when actual is 8Gi", conditional: "< 8Gi", - actual: 8 * 1024 * 1024 * 1024, + actual: 8 * 1024 * 1024 * 1024, // 8GiB expected: false, + expectErr: false, }, { name: "<= 8Gi when actual is 8Gi", conditional: "<= 8Gi", - actual: 8 * 1024 * 1024 * 1024, + actual: 8 * 1024 * 1024 * 1024, // 8GiB expected: true, + expectErr: false, }, { name: "<= 8Gi when actual is 16Gi", conditional: "<= 8Gi", - actual: 16 * 1024 * 1024 * 1024, + actual: 16 * 1024 * 1024 * 1024, // 16GiB expected: false, + expectErr: false, }, { name: "== 8Gi when actual is 16Gi", conditional: "== 8Gi", - actual: 16 * 1024 * 1024 * 1024, + actual: 16 * 1024 * 1024 * 1024, // 16GiB expected: false, + expectErr: false, }, { name: "== 8Gi when actual is 8Gi", conditional: "== 8Gi", - actual: 8 * 1024 * 1024 * 1024, + actual: 8 * 1024 * 1024 * 1024, // 8GiB expected: true, + expectErr: false, }, { name: "== 8000000000 when actual is 8000000000", conditional: "== 8000000000", - actual: 8 * 1000 * 1000 * 1000, + actual: 8 * 1000 * 1000 * 1000, // 8GB in decimal expected: true, + expectErr: false, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - req := require.New(t) + // Create the AnalyzeHostMemory object + analyzeHostMemory := AnalyzeHostMemory{} - actual, err := compareHostMemoryConditionalToActual(test.conditional, test.actual) - req.NoError(err) - - assert.Equal(t, test.expected, actual) + // Simulate the memory info as JSON-encoded data + memInfo := collect.MemoryInfo{ + Total: test.actual, + } + rawData, err := json.Marshal(memInfo) + require.NoError(t, err) + // Call the CheckCondition method + result, err := analyzeHostMemory.CheckCondition(test.conditional, rawData) + if test.expectErr { + require.Error(t, err) + } else { + require.NoError(t, err) + assert.Equal(t, test.expected, result) + } }) } } func TestAnalyzeHostMemory(t *testing.T) { tests := []struct { - name string - memoryInfo *collect.MemoryInfo - hostAnalyzer *troubleshootv1beta2.MemoryAnalyze - result []*AnalyzeResult - expectErr bool + name string + hostAnalyzer *troubleshootv1beta2.MemoryAnalyze + getCollectedFileContents func(string) ([]byte, error) + expectedResults []*AnalyzeResult + expectedError string }{ { - name: "Pass on memory available", - memoryInfo: &collect.MemoryInfo{ - Total: 8 * 1024 * 1024 * 1024, - }, + name: "Pass on memory available (local)", hostAnalyzer: &troubleshootv1beta2.MemoryAnalyze{ Outcomes: []*troubleshootv1beta2.Outcome{ { @@ -97,19 +116,27 @@ func TestAnalyzeHostMemory(t *testing.T) { }, }, }, - result: []*AnalyzeResult{ + getCollectedFileContents: func(path string) ([]byte, error) { + // Simulate local memory content retrieval + if path == collect.HostMemoryPath { + memoryInfo := collect.MemoryInfo{ + Total: 8 * 1024 * 1024 * 1024, // 8GiB + } + return json.Marshal(memoryInfo) + } + return nil, errors.New("file not found") + }, + expectedResults: []*AnalyzeResult{ { Title: "Amount of Memory", IsPass: true, Message: "System has at least 4Gi of memory", }, }, + expectedError: "", }, { - name: "Fail on memory available", - memoryInfo: &collect.MemoryInfo{ - Total: 8 * 1024 * 1024 * 1024, - }, + name: "Fail on memory available (remote node)", hostAnalyzer: &troubleshootv1beta2.MemoryAnalyze{ Outcomes: []*troubleshootv1beta2.Outcome{ { @@ -120,27 +147,33 @@ func TestAnalyzeHostMemory(t *testing.T) { }, }, }, - result: []*AnalyzeResult{ + getCollectedFileContents: func(path string) ([]byte, error) { + // Simulate remote node list and memory content retrieval + if path == constants.NODE_LIST_FILE { + nodeNames := nodeNames{Nodes: []string{"node1"}} + return json.Marshal(nodeNames) + } + if path == fmt.Sprintf("%s/node1/%s", collect.NodeInfoBaseDir, collect.HostMemoryFileName) { + memoryInfo := collect.MemoryInfo{ + Total: 8 * 1024 * 1024 * 1024, // 8GiB for remote node + } + return json.Marshal(memoryInfo) + } + return nil, errors.New("file not found") + }, + expectedResults: []*AnalyzeResult{ { - Title: "Amount of Memory", + Title: "Amount of Memory - Node node1", IsFail: true, Message: "System requires at least 16Gi of memory", }, }, + expectedError: "", }, { - name: "Warn on memory available", - memoryInfo: &collect.MemoryInfo{ - Total: 8 * 1024 * 1024 * 1024, - }, + name: "Warn on memory available (remote node)", hostAnalyzer: &troubleshootv1beta2.MemoryAnalyze{ Outcomes: []*troubleshootv1beta2.Outcome{ - { - Fail: &troubleshootv1beta2.SingleOutcome{ - When: "< 4Gi", - Message: "System requires at least 4Gi of memory", - }, - }, { Warn: &troubleshootv1beta2.SingleOutcome{ When: "<= 8Gi", @@ -149,97 +182,123 @@ func TestAnalyzeHostMemory(t *testing.T) { }, }, }, - result: []*AnalyzeResult{ + getCollectedFileContents: func(path string) ([]byte, error) { + // Simulate remote node list and memory content retrieval + if path == constants.NODE_LIST_FILE { + nodeNames := nodeNames{Nodes: []string{"node1"}} + return json.Marshal(nodeNames) + } + if path == fmt.Sprintf("%s/node1/%s", collect.NodeInfoBaseDir, collect.HostMemoryFileName) { + memoryInfo := collect.MemoryInfo{ + Total: 8 * 1024 * 1024 * 1024, // 8GiB for remote node + } + return json.Marshal(memoryInfo) + } + return nil, errors.New("file not found") + }, + expectedResults: []*AnalyzeResult{ { - Title: "Amount of Memory", + Title: "Amount of Memory - Node node1", IsWarn: true, Message: "System performs best with more than 8Gi of memory", }, }, + expectedError: "", }, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - req := require.New(t) - b, err := json.Marshal(test.memoryInfo) - if err != nil { - t.Fatal(err) - } - - getCollectedFileContents := func(filename string) ([]byte, error) { - return b, nil - } - - result, err := (&AnalyzeHostMemory{test.hostAnalyzer}).Analyze(getCollectedFileContents, nil) - if test.expectErr { - req.Error(err) - } else { - req.NoError(err) - } - - assert.Equal(t, test.result, result) - }) - } -} - -func TestHostMemoryAnalyze(t *testing.T) { - tt := []struct { - name string - memoryInfo collect.MemoryInfo - outcomes []*troubleshootv1beta2.Outcome - results []*AnalyzeResult - wantErr bool - }{ { - name: "fix for empty pass predicate", - memoryInfo: collect.MemoryInfo{ - Total: 16 * 1024 * 1024 * 1024, - }, - outcomes: []*troubleshootv1beta2.Outcome{ - { - Fail: &troubleshootv1beta2.SingleOutcome{ - When: "< 8Gi", - Message: "oops", + name: "Pass on empty pass predicate (local)", + hostAnalyzer: &troubleshootv1beta2.MemoryAnalyze{ + Outcomes: []*troubleshootv1beta2.Outcome{ + { + Fail: &troubleshootv1beta2.SingleOutcome{ + When: "< 8Gi", + Message: "System requires at least 8Gi of memory", + }, + }, + { + Pass: &troubleshootv1beta2.SingleOutcome{ + When: "", + Message: "Memory is sufficient", + }, }, }, + }, + getCollectedFileContents: func(path string) ([]byte, error) { + // Simulate local memory content retrieval + if path == collect.HostMemoryPath { + memoryInfo := collect.MemoryInfo{ + Total: 16 * 1024 * 1024 * 1024, // 16GiB + } + return json.Marshal(memoryInfo) + } + return nil, errors.New("file not found") + }, + expectedResults: []*AnalyzeResult{ { - Pass: &troubleshootv1beta2.SingleOutcome{ - When: "", - Message: "it passed", + Title: "Amount of Memory", + IsPass: true, + Message: "Memory is sufficient", + }, + }, + expectedError: "", + }, + { + name: "Fix for empty pass predicate", + hostAnalyzer: &troubleshootv1beta2.MemoryAnalyze{ + Outcomes: []*troubleshootv1beta2.Outcome{ + { + Fail: &troubleshootv1beta2.SingleOutcome{ + When: "< 8Gi", + Message: "oops", + }, + }, + { + Pass: &troubleshootv1beta2.SingleOutcome{ + When: "", + Message: "it passed", + }, }, }, }, - results: []*AnalyzeResult{ + getCollectedFileContents: func(path string) ([]byte, error) { + // Simulate local memory content retrieval + if path == collect.HostMemoryPath { + memoryInfo := collect.MemoryInfo{ + Total: 16 * 1024 * 1024 * 1024, // 16GiB + } + return json.Marshal(memoryInfo) + } + return nil, errors.New("file not found") + }, + expectedResults: []*AnalyzeResult{ { + Title: "Amount of Memory", IsPass: true, Message: "it passed", - Title: "Memory Test", }, }, + expectedError: "", }, } - for _, tc := range tt { - t.Run(tc.name, func(t *testing.T) { - fn := func(_ string) ([]byte, error) { - return json.Marshal(&tc.memoryInfo) + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + // Set up the AnalyzeHostMemory object + analyzeHostMemory := AnalyzeHostMemory{ + hostAnalyzer: test.hostAnalyzer, } - analyzer := AnalyzeHostMemory{ - hostAnalyzer: &troubleshootv1beta2.MemoryAnalyze{ - AnalyzeMeta: troubleshootv1beta2.AnalyzeMeta{ - CheckName: "Memory Test", - }, - Outcomes: tc.outcomes, - }, - } - results, err := analyzer.Analyze(fn, nil) - if tc.wantErr { - require.NotNil(t, err) - return + // Call the Analyze function + results, err := analyzeHostMemory.Analyze(test.getCollectedFileContents, nil) + + // Check for errors and compare results + if test.expectedError != "" { + require.Error(t, err) + assert.Contains(t, err.Error(), test.expectedError) + } else { + require.NoError(t, err) + assert.Equal(t, test.expectedResults, results) } - require.Nil(t, err) - require.Equal(t, tc.results, results) }) } } diff --git a/pkg/analyze/host_os_info.go b/pkg/analyze/host_os_info.go index d82a62e50..917a0c437 100644 --- a/pkg/analyze/host_os_info.go +++ b/pkg/analyze/host_os_info.go @@ -3,6 +3,7 @@ package analyzer import ( "encoding/json" "fmt" + "reflect" "regexp" "strings" @@ -10,18 +11,12 @@ import ( "github.com/pkg/errors" troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" "github.com/replicatedhq/troubleshoot/pkg/collect" - "github.com/replicatedhq/troubleshoot/pkg/constants" ) type AnalyzeHostOS struct { hostAnalyzer *troubleshootv1beta2.HostOSAnalyze } -type NodeOSInfo struct { - NodeName string - collect.HostOSInfo -} - func (a *AnalyzeHostOS) Title() string { return hostAnalyzerTitleOrDefault(a.hostAnalyzer.AnalyzeMeta, "Host OS Info") } @@ -33,69 +28,91 @@ func (a *AnalyzeHostOS) IsExcluded() (bool, error) { func (a *AnalyzeHostOS) Analyze( getCollectedFileContents func(string) ([]byte, error), findFiles getChildCollectedFileContents, ) ([]*AnalyzeResult, error) { - var nodesOSInfo []NodeOSInfo - result := AnalyzeResult{} - result.Title = a.Title() + result := AnalyzeResult{Title: a.Title()} + + // Use the generic function to collect both local and remote data + collectedContents, err := retrieveCollectedContents( + getCollectedFileContents, + collect.HostOSInfoPath, // Local path + collect.NodeInfoBaseDir, // Remote base directory + collect.HostInfoFileName, // Remote file name + ) + if err != nil { + return []*AnalyzeResult{&result}, err + } - // check if the host os info file exists (local mode) - contents, err := getCollectedFileContents(collect.HostOSInfoPath) + results, err := analyzeHostCollectorResults(collectedContents, a.hostAnalyzer.Outcomes, a.CheckCondition, a.Title()) if err != nil { - // check if the node list file exists (remote mode) - contents, err := getCollectedFileContents(constants.NODE_LIST_FILE) - if err != nil { - return []*AnalyzeResult{&result}, errors.Wrap(err, "failed to get collected file") - } + return nil, errors.Wrap(err, "failed to analyze OS version") + } - var nodes collect.HostOSInfoNodes - if err := json.Unmarshal(contents, &nodes); err != nil { - return []*AnalyzeResult{&result}, errors.Wrap(err, "failed to unmarshal host os info nodes") - } + return results, nil +} - // iterate over each node and analyze the host os info - for _, node := range nodes.Nodes { - contents, err := getCollectedFileContents(collect.NodeInfoBaseDir + "/" + node + "/" + collect.HostInfoFileName) - if err != nil { - return []*AnalyzeResult{&result}, errors.Wrap(err, "failed to get collected file") - } +// checkCondition checks the condition of the when clause +func (a *AnalyzeHostOS) CheckCondition(when string, data collectorData) (bool, error) { + rawData, ok := data.([]byte) + if !ok { + return false, fmt.Errorf("expected data to be []uint8 (raw bytes), got: %v", reflect.TypeOf(data)) + } - var osInfo collect.HostOSInfo - if err := json.Unmarshal(contents, &osInfo); err != nil { - return []*AnalyzeResult{&result}, errors.Wrap(err, "failed to unmarshal host os info") - } + var osInfo collect.HostOSInfo + if err := json.Unmarshal(rawData, &osInfo); err != nil { + return false, fmt.Errorf("failed to unmarshal data into HostOSInfo: %v", err) + } - nodesOSInfo = append(nodesOSInfo, NodeOSInfo{NodeName: node, HostOSInfo: osInfo}) - } + parts := strings.Split(when, " ") + if len(parts) < 3 { + return false, errors.New("when condition must have at least 3 parts") + } + expectedVer := fixVersion(parts[2]) + toleratedVer, err := semver.ParseTolerant(expectedVer) + if err != nil { + return false, errors.Wrapf(err, "failed to parse version: %s", expectedVer) + } + when = fmt.Sprintf("%s %v", parts[1], toleratedVer) + whenRange, err := semver.ParseRange(when) + if err != nil { + return false, errors.Wrapf(err, "failed to parse version range: %s", when) + } - results, err := analyzeOSVersionResult(nodesOSInfo, a.hostAnalyzer.Outcomes, a.Title()) + // Match the kernel version regardless of the platform + if parts[0] == "kernelVersion" { + fixedKernelVer := fixVersion(osInfo.KernelVersion) + toleratedKernelVer, err := semver.ParseTolerant(fixedKernelVer) if err != nil { - return []*AnalyzeResult{&result}, errors.Wrap(err, "failed to analyze os version result") + return false, errors.Wrapf(err, "failed to parse tolerant: %v", fixedKernelVer) + } + if whenRange(toleratedKernelVer) { + return true, nil } - return results, nil } - var osInfo collect.HostOSInfo - if err := json.Unmarshal(contents, &osInfo); err != nil { - return []*AnalyzeResult{&result}, errors.Wrap(err, "failed to unmarshal host os info") - } - nodesOSInfo = append(nodesOSInfo, NodeOSInfo{NodeName: "", HostOSInfo: osInfo}) - return analyzeOSVersionResult(nodesOSInfo, a.hostAnalyzer.Outcomes, a.Title()) -} - -func analyzeOSVersionResult(nodesOSInfo []NodeOSInfo, outcomes []*troubleshootv1beta2.Outcome, title string) ([]*AnalyzeResult, error) { - var results []*AnalyzeResult - for _, osInfo := range nodesOSInfo { - if title == "" { - title = "Host OS Info" + // Match platform version and kernel version, such as "centos-8.2-kernel == 8.2" + platform := parts[0] + kernelInfo := fmt.Sprintf("%s-%s-kernel", osInfo.Platform, osInfo.PlatformVersion) + if len(strings.Split(platform, "-")) == 3 && strings.Split(platform, "-")[2] == "kernel" { + if platform == kernelInfo { + fixedKernelVer := fixVersion(osInfo.KernelVersion) + toleratedKernelVer, err := semver.ParseTolerant(fixedKernelVer) + if err != nil { + return false, errors.Wrapf(err, "failed to parse tolerant: %v", fixedKernelVer) + } + if whenRange(toleratedKernelVer) { + return true, nil + } } - - analyzeResult, err := analyzeByOutcomes(outcomes, osInfo, title) + } else if platform == osInfo.Platform { + fixedDistVer := fixVersion(osInfo.PlatformVersion) + toleratedDistVer, err := semver.ParseTolerant(fixedDistVer) if err != nil { - return nil, errors.Wrap(err, "failed to analyze condition") + return false, errors.Wrapf(err, "failed to parse tolerant: %v", fixedDistVer) + } + if whenRange(toleratedDistVer) { + return true, nil } - results = append(results, analyzeResult...) } - - return results, nil + return false, nil } var rx = regexp.MustCompile(`^[0-9]+\.?[0-9]*\.?[0-9]*`) @@ -113,106 +130,3 @@ func fixVersion(versionStr string) string { version = strings.TrimRight(version, ".") return version } - -func analyzeByOutcomes(outcomes []*troubleshootv1beta2.Outcome, osInfo NodeOSInfo, title string) ([]*AnalyzeResult, error) { - var results []*AnalyzeResult - for _, outcome := range outcomes { - if osInfo.NodeName != "" { - title = fmt.Sprintf("%s - Node %s", title, osInfo.NodeName) - } - - result := AnalyzeResult{ - Title: title, - } - when := "" - message := "" - uri := "" - - if outcome.Fail != nil { - result.IsFail = true - when = outcome.Fail.When - message = outcome.Fail.Message - uri = outcome.Fail.URI - } else if outcome.Warn != nil { - result.IsWarn = true - when = outcome.Warn.When - message = outcome.Warn.Message - uri = outcome.Warn.URI - } else if outcome.Pass != nil { - result.IsPass = true - when = outcome.Pass.When - message = outcome.Pass.Message - uri = outcome.Pass.URI - } else { - return nil, errors.New("empty outcome") - } - - result.Message = message - result.URI = uri - // When is usually empty as the final case and should be treated as true - if when == "" { - results = append(results, &result) - return results, nil - } - - parts := strings.Split(when, " ") - if len(parts) < 3 { - return []*AnalyzeResult{&result}, errors.New("when condition must have at least 3 parts") - } - expectedVer := fixVersion(parts[2]) - toleratedVer, err := semver.ParseTolerant(expectedVer) - if err != nil { - return []*AnalyzeResult{&result}, errors.Wrapf(err, "failed to parse version: %s", expectedVer) - } - when = fmt.Sprintf("%s %v", parts[1], toleratedVer) - whenRange, err := semver.ParseRange(when) - if err != nil { - return []*AnalyzeResult{&result}, errors.Wrapf(err, "failed to parse version range: %s", when) - } - - // Match the kernel version regardless of the platform - // e.g "kernelVersion == 4.15" - if parts[0] == "kernelVersion" { - fixedKernelVer := fixVersion(osInfo.KernelVersion) - toleratedKernelVer, err := semver.ParseTolerant(fixedKernelVer) - if err != nil { - return []*AnalyzeResult{}, errors.Wrapf(err, "failed to parse tolerant: %v", fixedKernelVer) - } - if whenRange(toleratedKernelVer) { - results = append(results, &result) - return results, nil - } - } - - // Match the platform version and and kernel version passed in as - // "--kernel" e.g "centos-8.2-kernel == 8.2" - platform := parts[0] - kernelInfo := fmt.Sprintf("%s-%s-kernel", osInfo.Platform, osInfo.PlatformVersion) - if len(strings.Split(platform, "-")) == 3 && strings.Split(platform, "-")[2] == "kernel" { - if platform == kernelInfo { - fixedKernelVer := fixVersion(osInfo.KernelVersion) - toleratedKernelVer, err := semver.ParseTolerant(fixedKernelVer) - if err != nil { - return []*AnalyzeResult{&result}, errors.Wrapf(err, "failed to parse tolerant: %v", fixedKernelVer) - } - if whenRange(toleratedKernelVer) { - results = append(results, &result) - return results, nil - } - } - // Match the platform version - // e.g "centos == 8.2" - } else if platform == osInfo.Platform { - fixedDistVer := fixVersion(osInfo.PlatformVersion) - toleratedDistVer, err := semver.ParseTolerant(fixedDistVer) - if err != nil { - return []*AnalyzeResult{&result}, errors.Wrapf(err, "failed to parse tolerant: %v", fixedDistVer) - } - if whenRange(toleratedDistVer) { - results = append(results, &result) - return results, nil - } - } - } - return results, nil -} diff --git a/pkg/analyze/host_os_info_test.go b/pkg/analyze/host_os_info_test.go index 78511886a..00422df15 100644 --- a/pkg/analyze/host_os_info_test.go +++ b/pkg/analyze/host_os_info_test.go @@ -2,30 +2,106 @@ package analyzer import ( "encoding/json" + "fmt" "testing" + "github.com/pkg/errors" troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" "github.com/replicatedhq/troubleshoot/pkg/collect" + "github.com/replicatedhq/troubleshoot/pkg/constants" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func TestAnalyzeHostOS(t *testing.T) { +func TestAnalyzeHostOSCheckCondition(t *testing.T) { tests := []struct { - name string - hostInfo collect.HostOSInfo - hostAnalyzer *troubleshootv1beta2.HostOSAnalyze - result []*AnalyzeResult - expectErr bool + name string + conditional string + osInfo collect.HostOSInfo + expected bool + expectErr bool }{ { - name: "pass if ubuntu >= 0.1.2", - hostInfo: collect.HostOSInfo{ - Name: "myhost", - KernelVersion: "5.4.0-1034-gcp", - PlatformVersion: "00.1.2", + name: "kernelVersion == 4.15 when actual is 4.15", + conditional: "kernelVersion == 4.15", + osInfo: collect.HostOSInfo{ + KernelVersion: "4.15.0", + }, + expected: true, + expectErr: false, + }, + { + name: "kernelVersion < 4.15 when actual is 4.16", + conditional: "kernelVersion < 4.15", + osInfo: collect.HostOSInfo{ + KernelVersion: "4.16.0", + }, + expected: false, + expectErr: false, + }, + { + name: "centos == 8.2 when actual is 8.2", + conditional: "centos == 8.2", + osInfo: collect.HostOSInfo{ + Platform: "centos", + PlatformVersion: "8.2", + }, + expected: true, + expectErr: false, + }, + { + name: "ubuntu == 20.04 when actual is 18.04", + conditional: "ubuntu == 20.04", + osInfo: collect.HostOSInfo{ Platform: "ubuntu", + PlatformVersion: "18.04", }, + expected: false, + expectErr: false, + }, + { + name: "invalid conditional format", + conditional: "invalid conditional", + osInfo: collect.HostOSInfo{ + Platform: "ubuntu", + PlatformVersion: "18.04", + }, + expected: false, + expectErr: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + // Create the AnalyzeHostOS object + analyzeHostOS := AnalyzeHostOS{} + + // Simulate the OS info as JSON-encoded data + rawData, err := json.Marshal(test.osInfo) + require.NoError(t, err) + + // Call the CheckCondition method + result, err := analyzeHostOS.CheckCondition(test.conditional, rawData) + if test.expectErr { + require.Error(t, err) + } else { + require.NoError(t, err) + assert.Equal(t, test.expected, result) + } + }) + } +} + +func TestAnalyzeHostOS(t *testing.T) { + tests := []struct { + name string + hostAnalyzer *troubleshootv1beta2.HostOSAnalyze + getCollectedFileContents func(string) ([]byte, error) + result []*AnalyzeResult + expectErr bool + }{ + { + name: "successfully retrieve local content and analyze", hostAnalyzer: &troubleshootv1beta2.HostOSAnalyze{ Outcomes: []*troubleshootv1beta2.Outcome{ { @@ -41,6 +117,17 @@ func TestAnalyzeHostOS(t *testing.T) { }, }, }, + getCollectedFileContents: func(path string) ([]byte, error) { + if path == collect.HostOSInfoPath { + return json.Marshal(collect.HostOSInfo{ + Name: "myhost", + KernelVersion: "5.4.0-1034-gcp", + PlatformVersion: "00.1.2", + Platform: "ubuntu", + }) + } + return nil, errors.New("file not found") + }, result: []*AnalyzeResult{ { Title: "Host OS Info", @@ -48,22 +135,16 @@ func TestAnalyzeHostOS(t *testing.T) { Message: "supported distribution matches ubuntu >= 00.1.2", }, }, + expectErr: false, }, - { - name: "pass if ubuntu >= 1.0.2", - hostInfo: collect.HostOSInfo{ - Name: "myhost", - KernelVersion: "5.4.0-1034-gcp", - PlatformVersion: "1.0.2", - Platform: "ubuntu", - }, + name: "local content not found, retrieve and analyze remote content", hostAnalyzer: &troubleshootv1beta2.HostOSAnalyze{ Outcomes: []*troubleshootv1beta2.Outcome{ { Pass: &troubleshootv1beta2.SingleOutcome{ - When: "ubuntu >= 1.0.2", - Message: "supported distribution matches ubuntu >= 1.0.2", + When: "ubuntu == 18.04", + Message: "supported remote ubuntu 18.04", }, }, { @@ -73,90 +154,81 @@ func TestAnalyzeHostOS(t *testing.T) { }, }, }, + getCollectedFileContents: func(path string) ([]byte, error) { + if path == constants.NODE_LIST_FILE { + return json.Marshal(nodeNames{Nodes: []string{"node1"}}) + } + if path == fmt.Sprintf("%s/node1/%s", collect.NodeInfoBaseDir, collect.HostInfoFileName) { + return json.Marshal(collect.HostOSInfo{ + Name: "nodehost", + KernelVersion: "4.15.0-1034-aws", + PlatformVersion: "18.04", + Platform: "ubuntu", + }) + } + return nil, errors.New("file not found") + }, result: []*AnalyzeResult{ { - Title: "Host OS Info", + Title: "Host OS Info - Node node1", IsPass: true, - Message: "supported distribution matches ubuntu >= 1.0.2", + Message: "supported remote ubuntu 18.04", }, }, + expectErr: false, }, { - name: "pass if ubuntu >= 1.2.0", - hostInfo: collect.HostOSInfo{ - Name: "myhost", - KernelVersion: "5.4.0-1034-gcp", - PlatformVersion: "1.2.0", - Platform: "ubuntu", - }, + name: "fail to retrieve both local and remote content", hostAnalyzer: &troubleshootv1beta2.HostOSAnalyze{ Outcomes: []*troubleshootv1beta2.Outcome{ - { - Pass: &troubleshootv1beta2.SingleOutcome{ - When: "ubuntu >= 1.0.2", - Message: "supported distribution matches ubuntu >= 1.2.0", - }, - }, { Fail: &troubleshootv1beta2.SingleOutcome{ - Message: "unsupported distribution", + Message: "failed analysis", }, }, }, }, + getCollectedFileContents: func(path string) ([]byte, error) { + return nil, errors.New("file not found") + }, result: []*AnalyzeResult{ { - Title: "Host OS Info", - IsPass: true, - Message: "supported distribution matches ubuntu >= 1.2.0", + Title: "Host OS Info", }, }, + expectErr: true, }, { - name: "pass if ubuntu-1.2.0-kernel >= 1.2.0", - hostInfo: collect.HostOSInfo{ - Name: "myhost", - KernelVersion: "1.2.0-1034-gcp", - PlatformVersion: "1.2.0", - Platform: "centos", - }, + name: "error during remote content analysis", hostAnalyzer: &troubleshootv1beta2.HostOSAnalyze{ Outcomes: []*troubleshootv1beta2.Outcome{ - { - Pass: &troubleshootv1beta2.SingleOutcome{ - When: "centos-1.2.0-kernel >= 1.2.0", - Message: "supported kernel matches centos-1.2.0-kernel >= 1.2.0", - }, - }, { Fail: &troubleshootv1beta2.SingleOutcome{ - Message: "unsupported distribution", + Message: "analysis failed", }, }, }, }, - result: []*AnalyzeResult{ - { - Title: "Host OS Info", - IsPass: true, - Message: "supported kernel matches centos-1.2.0-kernel >= 1.2.0", - }, - }, + getCollectedFileContents: func(path string) ([]byte, error) { + if path == constants.NODE_LIST_FILE { + return json.Marshal(nodeNames{Nodes: []string{"node1"}}) + } + if path == fmt.Sprintf("%s/node1/%s", collect.NodeInfoBaseDir, collect.HostInfoFileName) { + return nil, errors.New("file not found") + } + return nil, errors.New("file not found") + }, + result: nil, + expectErr: true, }, { - name: "pass if ubuntu-0.1.2-kernel >= 0.1.2", - hostInfo: collect.HostOSInfo{ - Name: "myhost", - KernelVersion: "0.01.2-1034-gcp", - PlatformVersion: "8.2", - Platform: "centos", - }, + name: "pass if centos-1.2.0-kernel >= 1.2.0", hostAnalyzer: &troubleshootv1beta2.HostOSAnalyze{ Outcomes: []*troubleshootv1beta2.Outcome{ { Pass: &troubleshootv1beta2.SingleOutcome{ - When: "centos-8.2-kernel >= 0.01.2", - Message: "supported kernel matches centos-8.2-kernel >= 0.01.2", + When: "centos-1.2.0-kernel >= 1.2.0", + Message: "supported kernel matches centos-1.2.0-kernel >= 1.2.0", }, }, { @@ -166,307 +238,139 @@ func TestAnalyzeHostOS(t *testing.T) { }, }, }, + getCollectedFileContents: func(path string) ([]byte, error) { + if path == constants.NODE_LIST_FILE { + return json.Marshal(nodeNames{Nodes: []string{"node1"}}) + } + if path == fmt.Sprintf("%s/node1/%s", collect.NodeInfoBaseDir, collect.HostInfoFileName) { + return json.Marshal(collect.HostOSInfo{ + Name: "nodehost", + KernelVersion: "1.2.0-1034-aws", + PlatformVersion: "1.2.0", + Platform: "centos", + }) + } + return nil, errors.New("file not found") + }, result: []*AnalyzeResult{ { - Title: "Host OS Info", + Title: "Host OS Info - Node node1", IsPass: true, - Message: "supported kernel matches centos-8.2-kernel >= 0.01.2", + Message: "supported kernel matches centos-1.2.0-kernel >= 1.2.0", }, }, + expectErr: false, }, - { - name: "fail if ubuntu <= 11.04", - hostInfo: collect.HostOSInfo{ - Name: "myhost", - KernelVersion: "5.4.0-1034-gcp", - PlatformVersion: "11.04", - Platform: "ubuntu", - }, + name: "warn if ubuntu <= 16.04", hostAnalyzer: &troubleshootv1beta2.HostOSAnalyze{ Outcomes: []*troubleshootv1beta2.Outcome{ { - Fail: &troubleshootv1beta2.SingleOutcome{ - When: "ubuntu <= 11.04", - Message: "unsupported ubuntu version 11.04", + Warn: &troubleshootv1beta2.SingleOutcome{ + When: "ubuntu <= 16.04", + Message: "System performs best with Ubuntu version higher than 16.04", }, }, { Pass: &troubleshootv1beta2.SingleOutcome{ - Message: "supported distribution", + When: "ubuntu > 16.04", + Message: "Ubuntu version is sufficient", }, }, }, }, - result: []*AnalyzeResult{ - { - Title: "Host OS Info", - IsFail: true, - Message: "unsupported ubuntu version 11.04", - }, - }, - }, - { - name: "fail if none of the kernel distribution versions match", - hostInfo: collect.HostOSInfo{ - Name: "my-host", - KernelVersion: "4.4", - PlatformVersion: "18.04", - Platform: "ubuntu", - }, - hostAnalyzer: &troubleshootv1beta2.HostOSAnalyze{ - Outcomes: []*troubleshootv1beta2.Outcome{ - { - Pass: &troubleshootv1beta2.SingleOutcome{ - When: "centos-18.04-kernel > 4.15", - Message: "supported distribution matches centos-18.04-kernel >= 4.15", - }, - }, - { - Pass: &troubleshootv1beta2.SingleOutcome{ - When: "ubuntu-18.04-kernel > 4.15", - Message: "supported distribution matches ubuntu-18.04-kernel >= 4.15", - }, - }, - { - Warn: &troubleshootv1beta2.SingleOutcome{ - When: "ubuntu-16.04-kernel == 4.15", - Message: "supported distribution matches ubuntu-16.04-kernel == 4.15 ", - }, - }, - { - Fail: &troubleshootv1beta2.SingleOutcome{ - Message: "None matched, centos-18.04-kernel >= 4.15,ubuntu-18.04-kernel >= 4.15, supported distribution", - }, - }, - }, + getCollectedFileContents: func(path string) ([]byte, error) { + if path == collect.HostOSInfoPath { + return json.Marshal(collect.HostOSInfo{ + Name: "myhost", + KernelVersion: "4.15.0-1234-gcp", + PlatformVersion: "16.04", + Platform: "ubuntu", + }) + } + return nil, errors.New("file not found") }, result: []*AnalyzeResult{ { Title: "Host OS Info", - IsFail: true, - Message: "None matched, centos-18.04-kernel >= 4.15,ubuntu-18.04-kernel >= 4.15, supported distribution", + IsWarn: true, + Message: "System performs best with Ubuntu version higher than 16.04", }, }, + expectErr: false, }, { - name: "test if centos kernel > 4.15", - hostInfo: collect.HostOSInfo{ - Name: "my-host", - KernelVersion: "4.15", - PlatformVersion: "18.04", - Platform: "centos", - }, + name: "analyze multiple nodes with different OS info", hostAnalyzer: &troubleshootv1beta2.HostOSAnalyze{ Outcomes: []*troubleshootv1beta2.Outcome{ { Pass: &troubleshootv1beta2.SingleOutcome{ - When: "centos-18.04-kernel >= 4.15", - Message: "supported distribution matches centos-18.04-kernel >= 4.15", - }, - }, - { - Pass: &troubleshootv1beta2.SingleOutcome{ - When: "ubuntu-18.04-kernel > 4.15", - Message: "supported distribution matches ubuntu-18.04-kernel >= 4.15", - }, - }, - { - Warn: &troubleshootv1beta2.SingleOutcome{ - When: "ubuntu-16.04-kernel == 4.15", - Message: "supported distribution matches ubuntu-16.04-kernel == 4.15 ", + When: "ubuntu == 18.04", + Message: "supported ubuntu version", }, }, { Fail: &troubleshootv1beta2.SingleOutcome{ - Message: "None matched, centos-18.04-kernel >= 4.15,ubuntu-18.04-kernel >= 4.15, supported distribution", + Message: "unsupported ubuntu version", }, }, }, }, - result: []*AnalyzeResult{ - { - Title: "Host OS Info", - IsPass: true, - Message: "supported distribution matches centos-18.04-kernel >= 4.15", - }, - }, - }, - { - name: "test ubuntu 16 kernel >= 4.15-abc", - hostInfo: collect.HostOSInfo{ - Name: "my-host", - KernelVersion: "4.14-abc", - PlatformVersion: "16.04", - Platform: "ubuntu", - }, - hostAnalyzer: &troubleshootv1beta2.HostOSAnalyze{ - Outcomes: []*troubleshootv1beta2.Outcome{ - { - Pass: &troubleshootv1beta2.SingleOutcome{ - When: "ubuntu-16.04-kernel >= 4.14", - Message: "supported distribution match 4.14", - }, - }, - }, + getCollectedFileContents: func(path string) ([]byte, error) { + if path == constants.NODE_LIST_FILE { + return json.Marshal(nodeNames{Nodes: []string{"node1", "node2"}}) + } + if path == fmt.Sprintf("%s/node1/%s", collect.NodeInfoBaseDir, collect.HostInfoFileName) { + return json.Marshal(collect.HostOSInfo{ + Name: "nodehost", + KernelVersion: "4.15.0-1034-aws", + PlatformVersion: "18.04", + Platform: "ubuntu", + }) + } + if path == fmt.Sprintf("%s/node2/%s", collect.NodeInfoBaseDir, collect.HostInfoFileName) { + return json.Marshal(collect.HostOSInfo{ + Name: "nodehost", + KernelVersion: "4.15.0-1034-aws", + PlatformVersion: "16.04", + Platform: "ubuntu", + }) + } + return nil, errors.New("file not found") }, - result: []*AnalyzeResult{ { - Title: "Host OS Info", + Title: "Host OS Info - Node node1", IsPass: true, - Message: "supported distribution match 4.14", - }, - }, - }, - - { - name: "test kernelVersion >= 6.4.9-abc", - hostInfo: collect.HostOSInfo{ - Name: "my-host", - KernelVersion: "6.5.0-1024-gcp", - PlatformVersion: "22.04", - Platform: "ubuntu", - }, - hostAnalyzer: &troubleshootv1beta2.HostOSAnalyze{ - Outcomes: []*troubleshootv1beta2.Outcome{ - { - Pass: &troubleshootv1beta2.SingleOutcome{ - When: "kernelVersion >= 6.4.9-abc", - Message: "supported kernel version >= 6.4.9-abc", - }, - }, + Message: "supported ubuntu version", }, - }, - - result: []*AnalyzeResult{ { - Title: "Host OS Info", - IsPass: true, - Message: "supported kernel version >= 6.4.9-abc", + Title: "Host OS Info - Node node2", + IsFail: true, + Message: "unsupported ubuntu version", }, }, + expectErr: false, }, } + for _, test := range tests { t.Run(test.name, func(t *testing.T) { - req := require.New(t) - b, err := json.Marshal(test.hostInfo) - if err != nil { - t.Fatal(err) + // Set up the AnalyzeHostOS object with the custom hostAnalyzer per test + analyzeHostOS := AnalyzeHostOS{ + hostAnalyzer: test.hostAnalyzer, } - getCollectedFileContents := func(filename string) ([]byte, error) { - return b, nil - } + // Call the Analyze function + results, err := analyzeHostOS.Analyze(test.getCollectedFileContents, nil) - result, err := (&AnalyzeHostOS{test.hostAnalyzer}).Analyze(getCollectedFileContents, nil) + // Check for errors and compare results if test.expectErr { - req.Error(err) + require.Error(t, err) } else { - req.NoError(err) + require.NoError(t, err) + assert.Equal(t, test.result, results) } - - assert.Equal(t, test.result, result) - }) - } -} - -func TestAnalyzeOSVersionResult(t *testing.T) { - tests := []struct { - name string - outcomes []*troubleshootv1beta2.Outcome - nodeOSInfo []NodeOSInfo - expectResult []*AnalyzeResult - }{ - { - name: "pass if ubuntu >= 0.1.2", - nodeOSInfo: []NodeOSInfo{ - { - NodeName: "node1", - HostOSInfo: collect.HostOSInfo{ - Name: "myhost", - KernelVersion: "5.4.0-1034-gcp", - PlatformVersion: "00.1.2", - Platform: "ubuntu", - }, - }, - }, - outcomes: []*troubleshootv1beta2.Outcome{ - { - Pass: &troubleshootv1beta2.SingleOutcome{ - When: "ubuntu >= 00.1.2", - Message: "supported distribution matches ubuntu >= 00.1.2", - }, - }, - { - Fail: &troubleshootv1beta2.SingleOutcome{ - Message: "unsupported distribution", - }, - }, - }, - expectResult: []*AnalyzeResult{ - { - Title: "Host OS Info - Node node1", - IsPass: true, - Message: "supported distribution matches ubuntu >= 00.1.2", - }, - }, - }, - { - name: "fail if ubuntu <= 11.04", - nodeOSInfo: []NodeOSInfo{ - { - NodeName: "node1", - HostOSInfo: collect.HostOSInfo{ - Name: "myhost", - KernelVersion: "5.4.0-1034-gcp", - PlatformVersion: "11.04", - Platform: "ubuntu", - }, - }, - { - NodeName: "node2", - HostOSInfo: collect.HostOSInfo{ - Name: "myhost", - KernelVersion: "5.4.0-1034-gcp", - PlatformVersion: "11.04", - Platform: "ubuntu", - }, - }, - }, - outcomes: []*troubleshootv1beta2.Outcome{ - { - Fail: &troubleshootv1beta2.SingleOutcome{ - When: "ubuntu <= 11.04", - Message: "unsupported ubuntu version 11.04", - }, - }, - { - Pass: &troubleshootv1beta2.SingleOutcome{ - Message: "supported distribution", - }, - }, - }, - expectResult: []*AnalyzeResult{ - { - Title: "Host OS Info - Node node1", - IsFail: true, - Message: "unsupported ubuntu version 11.04", - }, - { - Title: "Host OS Info - Node node2", - IsFail: true, - Message: "unsupported ubuntu version 11.04", - }, - }, - }, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - result, err := analyzeOSVersionResult(test.nodeOSInfo, test.outcomes, "Host OS Info") - require.NoError(t, err) - assert.Equal(t, test.expectResult, result) }) } } diff --git a/pkg/collect/host_memory.go b/pkg/collect/host_memory.go index f7e4ad281..88273c8c9 100644 --- a/pkg/collect/host_memory.go +++ b/pkg/collect/host_memory.go @@ -14,6 +14,7 @@ type MemoryInfo struct { } const HostMemoryPath = `host-collectors/system/memory.json` +const HostMemoryFileName = `memory.json` type CollectHostMemory struct { hostCollector *troubleshootv1beta2.Memory diff --git a/pkg/supportbundle/collect.go b/pkg/supportbundle/collect.go index 303b68bcc..ec8e680c2 100644 --- a/pkg/supportbundle/collect.go +++ b/pkg/supportbundle/collect.go @@ -23,21 +23,10 @@ import ( "k8s.io/client-go/kubernetes" ) -type FilteredCollector struct { - Spec troubleshootv1beta2.HostCollect - Collector collect.HostCollector -} - func runHostCollectors(ctx context.Context, hostCollectors []*troubleshootv1beta2.HostCollect, additionalRedactors *troubleshootv1beta2.Redactor, bundlePath string, opts SupportBundleCreateOpts) (collect.CollectorResult, error) { collectSpecs := append([]*troubleshootv1beta2.HostCollect{}, hostCollectors...) collectedData := make(map[string][]byte) - // Filter out excluded collectors - filteredCollectors, err := filterHostCollectors(ctx, collectSpecs, bundlePath, opts) - if err != nil { - return nil, err - } - if opts.RunHostCollectorsInPod { if err := checkRemoteCollectorRBAC(ctx, opts.KubernetesRestConfig, "Remote Host Collectors", opts.Namespace); err != nil { if rbacErr, ok := err.(*RBACPermissionError); ok { @@ -52,11 +41,11 @@ func runHostCollectors(ctx context.Context, hostCollectors []*troubleshootv1beta return nil, err } } - if err := collectRemoteHost(ctx, filteredCollectors, bundlePath, opts, collectedData); err != nil { + if err := collectRemoteHost(ctx, collectSpecs, bundlePath, opts, collectedData); err != nil { return nil, err } } else { - if err := collectHost(ctx, filteredCollectors, opts, collectedData); err != nil { + if err := collectHost(ctx, collectSpecs, bundlePath, opts, collectedData); err != nil { return nil, err } } @@ -219,27 +208,38 @@ func getAnalysisFile(analyzeResults []*analyze.AnalyzeResult) (io.Reader, error) } // collectRemoteHost runs remote host collectors sequentially -func collectRemoteHost(ctx context.Context, filteredCollectors []FilteredCollector, bundlePath string, opts SupportBundleCreateOpts, collectedData map[string][]byte) error { +func collectRemoteHost(ctx context.Context, collectSpecs []*troubleshootv1beta2.HostCollect, bundlePath string, opts SupportBundleCreateOpts, collectedData map[string][]byte) error { opts.KubernetesRestConfig.QPS = constants.DEFAULT_CLIENT_QPS opts.KubernetesRestConfig.Burst = constants.DEFAULT_CLIENT_BURST opts.KubernetesRestConfig.UserAgent = fmt.Sprintf("%s/%s", constants.DEFAULT_CLIENT_USER_AGENT, version.Version()) // Run remote collectors sequentially - for _, c := range filteredCollectors { - collector := c.Collector - spec := c.Spec - - // Send progress event: starting the collector - opts.ProgressChan <- fmt.Sprintf("[%s] Running host collector...", collector.Title()) + for _, spec := range collectSpecs { + collector, ok := collect.GetHostCollector(spec, bundlePath) + if !ok { + opts.ProgressChan <- "Host collector not found" + continue + } // Start a span for tracing _, span := otel.Tracer(constants.LIB_TRACER_NAME).Start(ctx, collector.Title()) span.SetAttributes(attribute.String("type", reflect.TypeOf(collector).String())) + isExcluded, _ := collector.IsExcluded() + if isExcluded { + opts.ProgressChan <- fmt.Sprintf("[%s] Excluding host collector", collector.Title()) + span.SetAttributes(attribute.Bool(constants.EXCLUDED, true)) + span.End() + continue + } + + // Send progress event: starting the collector + opts.ProgressChan <- fmt.Sprintf("[%s] Running host collector...", collector.Title()) + // Parameters for remote collection params := &collect.RemoteCollectParams{ ProgressChan: opts.ProgressChan, - HostCollector: &spec, + HostCollector: spec, BundlePath: bundlePath, ClientConfig: opts.KubernetesRestConfig, Image: "replicated/troubleshoot:latest", @@ -273,10 +273,14 @@ func collectRemoteHost(ctx context.Context, filteredCollectors []FilteredCollect } // collectHost runs host collectors sequentially -func collectHost(ctx context.Context, filteredCollectors []FilteredCollector, opts SupportBundleCreateOpts, collectedData map[string][]byte) error { +func collectHost(ctx context.Context, collectSpecs []*troubleshootv1beta2.HostCollect, bundlePath string, opts SupportBundleCreateOpts, collectedData map[string][]byte) error { // Run local collectors sequentially - for _, c := range filteredCollectors { - collector := c.Collector + for _, spec := range collectSpecs { + collector, ok := collect.GetHostCollector(spec, bundlePath) + if !ok { + opts.ProgressChan <- "Host collector not found" + continue + } // Send progress event: starting the collector opts.ProgressChan <- fmt.Sprintf("[%s] Running host collector...", collector.Title()) @@ -285,6 +289,14 @@ func collectHost(ctx context.Context, filteredCollectors []FilteredCollector, op _, span := otel.Tracer(constants.LIB_TRACER_NAME).Start(ctx, collector.Title()) span.SetAttributes(attribute.String("type", reflect.TypeOf(collector).String())) + isExcluded, _ := collector.IsExcluded() + if isExcluded { + opts.ProgressChan <- fmt.Sprintf("[%s] Excluding host collector", collector.Title()) + span.SetAttributes(attribute.Bool(constants.EXCLUDED, true)) + span.End() + continue + } + // Run local collector sequentially result, err := collector.Collect(opts.ProgressChan) if err != nil { @@ -324,34 +336,3 @@ func getGlobalRedactors(additionalRedactors *troubleshootv1beta2.Redactor) []*tr } return []*troubleshootv1beta2.Redact{} } - -// filterHostCollectors filters out excluded collectors and returns a list of collectors to run -func filterHostCollectors(ctx context.Context, collectSpecs []*troubleshootv1beta2.HostCollect, bundlePath string, opts SupportBundleCreateOpts) ([]FilteredCollector, error) { - var filteredCollectors []FilteredCollector - - for _, desiredCollector := range collectSpecs { - collector, ok := collect.GetHostCollector(desiredCollector, bundlePath) - if !ok { - opts.ProgressChan <- "Host collector not found" - continue - } - - _, span := otel.Tracer(constants.LIB_TRACER_NAME).Start(ctx, collector.Title()) - span.SetAttributes(attribute.String("type", reflect.TypeOf(collector).String())) - - isExcluded, _ := collector.IsExcluded() - if isExcluded { - opts.ProgressChan <- fmt.Sprintf("[%s] Excluding host collector", collector.Title()) - span.SetAttributes(attribute.Bool(constants.EXCLUDED, true)) - span.End() - continue - } - - filteredCollectors = append(filteredCollectors, FilteredCollector{ - Spec: *desiredCollector, - Collector: collector, - }) - } - - return filteredCollectors, nil -} diff --git a/pkg/supportbundle/collect_test.go b/pkg/supportbundle/collect_test.go deleted file mode 100644 index 0dfda3e68..000000000 --- a/pkg/supportbundle/collect_test.go +++ /dev/null @@ -1,45 +0,0 @@ -package supportbundle - -import ( - "context" - "testing" - - v1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/trace/noop" -) - -func Test_filterHostCollectors(t *testing.T) { - otel.SetTracerProvider(noop.NewTracerProvider()) - - testCases := []struct { - name string - collectSpecs []*v1beta2.HostCollect - bundlePath string - opts SupportBundleCreateOpts - expectedResult []FilteredCollector - expectedError error - }{ - { - name: "nil host collectors spec", - collectSpecs: []*v1beta2.HostCollect{}, - bundlePath: "/tmp", - opts: SupportBundleCreateOpts{ - ProgressChan: make(chan interface{}, 10), - }, - expectedResult: []FilteredCollector{}, - expectedError: nil, - }, - } - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - filtered, err := filterHostCollectors(context.TODO(), tc.collectSpecs, tc.bundlePath, tc.opts) - if err != tc.expectedError { - t.Fatalf("expected error %v, got %v", tc.expectedError, err) - } - if len(filtered) != len(tc.expectedResult) { - t.Fatalf("expected %d filtered collectors, got %d", len(tc.expectedResult), len(filtered)) - } - }) - } -} From 0d21eed5f8091dabe63e72f342fc4ac90c3d7758 Mon Sep 17 00:00:00 2001 From: Dexter Yan Date: Tue, 22 Oct 2024 13:07:44 +1300 Subject: [PATCH 15/25] fix(support): add missing host collectors for ParseSupportBundle (#1656) * fix(support): add missing host collectors for ParseSupportBundle * update * add host ananlyers --- config/crds/troubleshoot.sh_collectors.yaml | 534 +++++++++++++ .../troubleshoot/v1beta2/collector_types.go | 1 + .../v1beta2/zz_generated.deepcopy.go | 11 + pkg/supportbundle/load.go | 3 +- pkg/supportbundle/load_test.go | 94 +++ schemas/collector-troubleshoot-v1beta2.json | 754 ++++++++++++++++++ 6 files changed, 1396 insertions(+), 1 deletion(-) create mode 100644 pkg/supportbundle/load_test.go diff --git a/config/crds/troubleshoot.sh_collectors.yaml b/config/crds/troubleshoot.sh_collectors.yaml index 41665245c..d30a32fc2 100644 --- a/config/crds/troubleshoot.sh_collectors.yaml +++ b/config/crds/troubleshoot.sh_collectors.yaml @@ -16857,6 +16857,540 @@ spec: type: object type: object type: array + hostCollectors: + items: + properties: + blockDevices: + properties: + collectorName: + type: string + exclude: + type: BoolString + type: object + certificate: + properties: + certificatePath: + type: string + collectorName: + type: string + exclude: + type: BoolString + keyPath: + type: string + required: + - certificatePath + - keyPath + type: object + certificatesCollection: + properties: + collectorName: + type: string + exclude: + type: BoolString + paths: + items: + type: string + type: array + required: + - paths + type: object + cgroups: + properties: + collectorName: + type: string + exclude: + type: BoolString + mountPoint: + type: string + type: object + copy: + properties: + collectorName: + type: string + exclude: + type: BoolString + path: + type: string + required: + - path + type: object + cpu: + properties: + collectorName: + type: string + exclude: + type: BoolString + type: object + diskUsage: + properties: + collectorName: + type: string + exclude: + type: BoolString + path: + type: string + required: + - path + type: object + dns: + properties: + collectorName: + type: string + exclude: + type: BoolString + hostnames: + items: + type: string + type: array + required: + - hostnames + type: object + filesystemPerformance: + description: |- + FilesystemPerformance benchmarks sequential write latency on a single file. + The optional background IOPS feature attempts to mimic real-world conditions by running read and + write workloads prior to and during benchmark execution. + properties: + backgroundIOPSWarmupSeconds: + description: How long to run the background IOPS read and + write workloads prior to starting the benchmarks. + type: integer + backgroundReadIOPS: + description: |- + The target read IOPS to run while benchmarking. This is a limit and there is no guarantee + it will be reached. This is the total IOPS for all background read jobs. + type: integer + backgroundReadIOPSJobs: + description: |- + Number of threads to use for background read IOPS. This should be set high enough to reach + the target specified in BackgrounReadIOPS. + type: integer + backgroundWriteIOPS: + description: |- + The target write IOPS to run while benchmarking. This is a limit and there is no guarantee + it will be reached. This is the total IOPS for all background write jobs. + type: integer + backgroundWriteIOPSJobs: + description: |- + Number of threads to use for background write IOPS. This should be set high enough to reach + the target specified in BackgroundWriteIOPS. + Example: If BackgroundWriteIOPS is 100 and write latency is 10ms then a single job would + barely be able to reach 100 IOPS so this should be at least 2. + type: integer + collectorName: + type: string + datasync: + description: |- + Whether to call datasync on the file after each write. Skipped if Sync is also true. Does not + apply to background IOPS task. + type: boolean + directory: + description: The directory where the benchmark will create + files. + type: string + enableBackgroundIOPS: + description: Enable the background IOPS feature. + type: boolean + exclude: + type: BoolString + fileSize: + description: |- + The size of the file used in the benchmark. The number of IO operations for the benchmark + will be FileSize / OperationSizeBytes. Accepts valid Kubernetes resource units such as Mi. + type: string + operationSize: + description: |- + The size of each write operation performed while benchmarking. This does not apply to the + background IOPS feature if enabled, since those must be fixed at 4096. + format: int64 + type: integer + runTime: + description: |- + Limit runtime. The test will run until it completes the configured I/O workload or until it + has run for this specified amount of time, whichever occurs first. When the unit is omitted, + the value is interpreted in seconds. Defaults to 120 seconds. Set to "0" to disable. + type: string + sync: + description: Whether to call sync on the file after each + write. Does not apply to background IOPS task. + type: boolean + timeout: + description: Total timeout, including background IOPS setup + and warmup if enabled. + type: string + required: + - backgroundIOPSWarmupSeconds + - backgroundReadIOPS + - backgroundReadIOPSJobs + - backgroundWriteIOPS + - backgroundWriteIOPSJobs + - enableBackgroundIOPS + type: object + hostOS: + properties: + collectorName: + type: string + exclude: + type: BoolString + type: object + hostServices: + properties: + collectorName: + type: string + exclude: + type: BoolString + type: object + http: + properties: + collectorName: + type: string + exclude: + type: BoolString + get: + properties: + headers: + additionalProperties: + type: string + type: object + insecureSkipVerify: + type: boolean + timeout: + description: |- + Timeout is the time to wait for a server's response. Its a duration e.g 15s, 2h30m. + Missing value or empty string or means no timeout. + type: string + url: + type: string + required: + - url + type: object + post: + properties: + body: + type: string + headers: + additionalProperties: + type: string + type: object + insecureSkipVerify: + type: boolean + timeout: + description: |- + Timeout is the time to wait for a server's response. Its a duration e.g 15s, 2h30m. + Missing value or empty string or means no timeout. + type: string + url: + type: string + required: + - url + type: object + put: + properties: + body: + type: string + headers: + additionalProperties: + type: string + type: object + insecureSkipVerify: + type: boolean + timeout: + description: |- + Timeout is the time to wait for a server's response. Its a duration e.g 15s, 2h30m. + Missing value or empty string or means no timeout. + type: string + url: + type: string + required: + - url + type: object + type: object + httpLoadBalancer: + properties: + address: + type: string + collectorName: + type: string + exclude: + type: BoolString + path: + type: string + port: + type: integer + timeout: + type: string + required: + - address + - path + - port + type: object + ipv4Interfaces: + properties: + collectorName: + type: string + exclude: + type: BoolString + type: object + journald: + properties: + collectorName: + type: string + dmesg: + type: boolean + exclude: + type: BoolString + lines: + type: integer + output: + type: string + reverse: + type: boolean + since: + type: string + system: + type: boolean + timeout: + type: string + units: + items: + type: string + type: array + until: + type: string + utc: + type: boolean + type: object + kernelConfigs: + properties: + collectorName: + type: string + exclude: + type: BoolString + type: object + kernelModules: + properties: + collectorName: + type: string + exclude: + type: BoolString + type: object + kubernetes: + properties: + collectorName: + type: string + exclude: + type: BoolString + type: object + memory: + properties: + collectorName: + type: string + exclude: + type: BoolString + type: object + run: + properties: + args: + items: + type: string + type: array + collectorName: + type: string + command: + type: string + env: + items: + type: string + type: array + exclude: + type: BoolString + ignoreParentEnvs: + type: boolean + inheritEnvs: + items: + type: string + type: array + input: + additionalProperties: + type: string + type: object + outputDir: + type: string + timeout: + type: string + required: + - args + - command + type: object + subnetAvailable: + properties: + CIDRRangeAlloc: + type: string + collectorName: + type: string + desiredCIDR: + type: integer + exclude: + type: BoolString + required: + - CIDRRangeAlloc + - desiredCIDR + type: object + systemPackages: + properties: + amzn: + items: + type: string + type: array + amzn2: + items: + type: string + type: array + centos: + items: + type: string + type: array + centos7: + items: + type: string + type: array + centos8: + items: + type: string + type: array + centos9: + items: + type: string + type: array + collectorName: + type: string + exclude: + type: BoolString + ol: + items: + type: string + type: array + ol7: + items: + type: string + type: array + ol8: + items: + type: string + type: array + ol9: + items: + type: string + type: array + rhel: + items: + type: string + type: array + rhel7: + items: + type: string + type: array + rhel8: + items: + type: string + type: array + rhel9: + items: + type: string + type: array + rocky: + items: + type: string + type: array + rocky8: + items: + type: string + type: array + rocky9: + items: + type: string + type: array + ubuntu: + items: + type: string + type: array + ubuntu16: + items: + type: string + type: array + ubuntu18: + items: + type: string + type: array + ubuntu20: + items: + type: string + type: array + type: object + tcpConnect: + properties: + address: + type: string + collectorName: + type: string + exclude: + type: BoolString + timeout: + type: string + required: + - address + type: object + tcpLoadBalancer: + properties: + address: + type: string + collectorName: + type: string + exclude: + type: BoolString + port: + type: integer + timeout: + type: string + required: + - address + - port + type: object + tcpPortStatus: + properties: + collectorName: + type: string + exclude: + type: BoolString + interface: + type: string + port: + type: integer + required: + - port + type: object + time: + properties: + collectorName: + type: string + exclude: + type: BoolString + type: object + udpPortStatus: + properties: + collectorName: + type: string + exclude: + type: BoolString + interface: + type: string + port: + type: integer + required: + - port + type: object + type: object + type: array uri: type: string type: object diff --git a/pkg/apis/troubleshoot/v1beta2/collector_types.go b/pkg/apis/troubleshoot/v1beta2/collector_types.go index e95f51ce2..aac221f9e 100644 --- a/pkg/apis/troubleshoot/v1beta2/collector_types.go +++ b/pkg/apis/troubleshoot/v1beta2/collector_types.go @@ -34,6 +34,7 @@ type AfterCollection struct { // CollectorSpec defines the desired state of Collector type CollectorSpec struct { Collectors []*Collect `json:"collectors,omitempty" yaml:"collectors,omitempty"` + HostCollectors []*HostCollect `json:"hostCollectors,omitempty" yaml:"hostCollectors,omitempty"` AfterCollection []*AfterCollection `json:"afterCollection,omitempty" yaml:"afterCollection,omitempty"` Uri string `json:"uri,omitempty" yaml:"uri,omitempty"` } diff --git a/pkg/apis/troubleshoot/v1beta2/zz_generated.deepcopy.go b/pkg/apis/troubleshoot/v1beta2/zz_generated.deepcopy.go index 5b40aca8f..af210443e 100644 --- a/pkg/apis/troubleshoot/v1beta2/zz_generated.deepcopy.go +++ b/pkg/apis/troubleshoot/v1beta2/zz_generated.deepcopy.go @@ -1094,6 +1094,17 @@ func (in *CollectorSpec) DeepCopyInto(out *CollectorSpec) { } } } + if in.HostCollectors != nil { + in, out := &in.HostCollectors, &out.HostCollectors + *out = make([]*HostCollect, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(HostCollect) + (*in).DeepCopyInto(*out) + } + } + } if in.AfterCollection != nil { in, out := &in.AfterCollection, &out.AfterCollection *out = make([]*AfterCollection, len(*in)) diff --git a/pkg/supportbundle/load.go b/pkg/supportbundle/load.go index a5152cf3b..888110cc0 100644 --- a/pkg/supportbundle/load.go +++ b/pkg/supportbundle/load.go @@ -38,7 +38,6 @@ func GetSupportBundleFromURI(bundleURI string) (*troubleshootv1beta2.SupportBund } // ParseSupportBundle parses a support bundle from a byte array into a SupportBundle object -// We will deprecate this in favour of use loader.LoadSpecs once the new API is stable func ParseSupportBundle(doc []byte, followURI bool) (*troubleshootv1beta2.SupportBundle, error) { doc, err := docrewrite.ConvertToV1Beta2(doc) if err != nil { @@ -65,7 +64,9 @@ func ParseSupportBundle(doc []byte, followURI bool) (*troubleshootv1beta2.Suppor ObjectMeta: collector.ObjectMeta, Spec: troubleshootv1beta2.SupportBundleSpec{ Collectors: collector.Spec.Collectors, + HostCollectors: collector.Spec.HostCollectors, Analyzers: []*troubleshootv1beta2.Analyze{}, + HostAnalyzers: []*troubleshootv1beta2.HostAnalyze{}, AfterCollection: collector.Spec.AfterCollection, }, } diff --git a/pkg/supportbundle/load_test.go b/pkg/supportbundle/load_test.go new file mode 100644 index 000000000..e4d54911e --- /dev/null +++ b/pkg/supportbundle/load_test.go @@ -0,0 +1,94 @@ +package supportbundle + +import ( + "reflect" + "testing" + + troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func Test_ParseSupportBundle(t *testing.T) { + tests := []struct { + name string + doc []byte + followURI bool + want *troubleshootv1beta2.SupportBundle + wantErr bool + }{ + { + name: "Parse Host Collectors", + doc: []byte(` +apiVersion: troubleshoot.sh/v1beta2 +kind: SupportBundle +metadata: + name: test +spec: + hostCollectors: + - hostOS: {} +`), + followURI: false, + want: &troubleshootv1beta2.SupportBundle{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "troubleshoot.sh/v1beta2", + Kind: "SupportBundle", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + }, + Spec: troubleshootv1beta2.SupportBundleSpec{ + HostCollectors: []*troubleshootv1beta2.HostCollect{ + { + HostOS: &troubleshootv1beta2.HostOS{}, + }, + }, + }, + }, + wantErr: false, + }, + { + name: "Parse Collectors", + doc: []byte(` +apiVersion: troubleshoot.sh/v1beta2 +kind: SupportBundle +metadata: + name: test +spec: + collectors: + - clusterInfo: {} +`), + followURI: false, + want: &troubleshootv1beta2.SupportBundle{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "troubleshoot.sh/v1beta2", + Kind: "SupportBundle", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + }, + Spec: troubleshootv1beta2.SupportBundleSpec{ + Collectors: []*troubleshootv1beta2.Collect{ + { + ClusterInfo: &troubleshootv1beta2.ClusterInfo{}, + }, + }, + }, + }, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ParseSupportBundle(tt.doc, tt.followURI) + if (err != nil) != tt.wantErr { + t.Errorf("ParseSupportBundle() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("ParseSupportBundle() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/schemas/collector-troubleshoot-v1beta2.json b/schemas/collector-troubleshoot-v1beta2.json index a539ae297..c6b6105a8 100644 --- a/schemas/collector-troubleshoot-v1beta2.json +++ b/schemas/collector-troubleshoot-v1beta2.json @@ -14367,6 +14367,760 @@ } } }, + "hostCollectors": { + "type": "array", + "items": { + "type": "object", + "properties": { + "blockDevices": { + "type": "object", + "properties": { + "collectorName": { + "type": "string" + }, + "exclude": { + "oneOf": [{"type": "string"},{"type": "boolean"}] + } + } + }, + "certificate": { + "type": "object", + "required": [ + "certificatePath", + "keyPath" + ], + "properties": { + "certificatePath": { + "type": "string" + }, + "collectorName": { + "type": "string" + }, + "exclude": { + "oneOf": [{"type": "string"},{"type": "boolean"}] + }, + "keyPath": { + "type": "string" + } + } + }, + "certificatesCollection": { + "type": "object", + "required": [ + "paths" + ], + "properties": { + "collectorName": { + "type": "string" + }, + "exclude": { + "oneOf": [{"type": "string"},{"type": "boolean"}] + }, + "paths": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "cgroups": { + "type": "object", + "properties": { + "collectorName": { + "type": "string" + }, + "exclude": { + "oneOf": [{"type": "string"},{"type": "boolean"}] + }, + "mountPoint": { + "type": "string" + } + } + }, + "copy": { + "type": "object", + "required": [ + "path" + ], + "properties": { + "collectorName": { + "type": "string" + }, + "exclude": { + "oneOf": [{"type": "string"},{"type": "boolean"}] + }, + "path": { + "type": "string" + } + } + }, + "cpu": { + "type": "object", + "properties": { + "collectorName": { + "type": "string" + }, + "exclude": { + "oneOf": [{"type": "string"},{"type": "boolean"}] + } + } + }, + "diskUsage": { + "type": "object", + "required": [ + "path" + ], + "properties": { + "collectorName": { + "type": "string" + }, + "exclude": { + "oneOf": [{"type": "string"},{"type": "boolean"}] + }, + "path": { + "type": "string" + } + } + }, + "dns": { + "type": "object", + "required": [ + "hostnames" + ], + "properties": { + "collectorName": { + "type": "string" + }, + "exclude": { + "oneOf": [{"type": "string"},{"type": "boolean"}] + }, + "hostnames": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "filesystemPerformance": { + "description": "FilesystemPerformance benchmarks sequential write latency on a single file.\nThe optional background IOPS feature attempts to mimic real-world conditions by running read and\nwrite workloads prior to and during benchmark execution.", + "type": "object", + "required": [ + "backgroundIOPSWarmupSeconds", + "backgroundReadIOPS", + "backgroundReadIOPSJobs", + "backgroundWriteIOPS", + "backgroundWriteIOPSJobs", + "enableBackgroundIOPS" + ], + "properties": { + "backgroundIOPSWarmupSeconds": { + "description": "How long to run the background IOPS read and write workloads prior to starting the benchmarks.", + "type": "integer" + }, + "backgroundReadIOPS": { + "description": "The target read IOPS to run while benchmarking. This is a limit and there is no guarantee\nit will be reached. This is the total IOPS for all background read jobs.", + "type": "integer" + }, + "backgroundReadIOPSJobs": { + "description": "Number of threads to use for background read IOPS. This should be set high enough to reach\nthe target specified in BackgrounReadIOPS.", + "type": "integer" + }, + "backgroundWriteIOPS": { + "description": "The target write IOPS to run while benchmarking. This is a limit and there is no guarantee\nit will be reached. This is the total IOPS for all background write jobs.", + "type": "integer" + }, + "backgroundWriteIOPSJobs": { + "description": "Number of threads to use for background write IOPS. This should be set high enough to reach\nthe target specified in BackgroundWriteIOPS.\nExample: If BackgroundWriteIOPS is 100 and write latency is 10ms then a single job would\nbarely be able to reach 100 IOPS so this should be at least 2.", + "type": "integer" + }, + "collectorName": { + "type": "string" + }, + "datasync": { + "description": "Whether to call datasync on the file after each write. Skipped if Sync is also true. Does not\napply to background IOPS task.", + "type": "boolean" + }, + "directory": { + "description": "The directory where the benchmark will create files.", + "type": "string" + }, + "enableBackgroundIOPS": { + "description": "Enable the background IOPS feature.", + "type": "boolean" + }, + "exclude": { + "oneOf": [{"type": "string"},{"type": "boolean"}] + }, + "fileSize": { + "description": "The size of the file used in the benchmark. The number of IO operations for the benchmark\nwill be FileSize / OperationSizeBytes. Accepts valid Kubernetes resource units such as Mi.", + "type": "string" + }, + "operationSize": { + "description": "The size of each write operation performed while benchmarking. This does not apply to the\nbackground IOPS feature if enabled, since those must be fixed at 4096.", + "type": "integer", + "format": "int64" + }, + "runTime": { + "description": "Limit runtime. The test will run until it completes the configured I/O workload or until it\nhas run for this specified amount of time, whichever occurs first. When the unit is omitted,\nthe value is interpreted in seconds. Defaults to 120 seconds. Set to \"0\" to disable.", + "type": "string" + }, + "sync": { + "description": "Whether to call sync on the file after each write. Does not apply to background IOPS task.", + "type": "boolean" + }, + "timeout": { + "description": "Total timeout, including background IOPS setup and warmup if enabled.", + "type": "string" + } + } + }, + "hostOS": { + "type": "object", + "properties": { + "collectorName": { + "type": "string" + }, + "exclude": { + "oneOf": [{"type": "string"},{"type": "boolean"}] + } + } + }, + "hostServices": { + "type": "object", + "properties": { + "collectorName": { + "type": "string" + }, + "exclude": { + "oneOf": [{"type": "string"},{"type": "boolean"}] + } + } + }, + "http": { + "type": "object", + "properties": { + "collectorName": { + "type": "string" + }, + "exclude": { + "oneOf": [{"type": "string"},{"type": "boolean"}] + }, + "get": { + "type": "object", + "required": [ + "url" + ], + "properties": { + "headers": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "insecureSkipVerify": { + "type": "boolean" + }, + "timeout": { + "description": "Timeout is the time to wait for a server's response. Its a duration e.g 15s, 2h30m.\nMissing value or empty string or means no timeout.", + "type": "string" + }, + "url": { + "type": "string" + } + } + }, + "post": { + "type": "object", + "required": [ + "url" + ], + "properties": { + "body": { + "type": "string" + }, + "headers": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "insecureSkipVerify": { + "type": "boolean" + }, + "timeout": { + "description": "Timeout is the time to wait for a server's response. Its a duration e.g 15s, 2h30m.\nMissing value or empty string or means no timeout.", + "type": "string" + }, + "url": { + "type": "string" + } + } + }, + "put": { + "type": "object", + "required": [ + "url" + ], + "properties": { + "body": { + "type": "string" + }, + "headers": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "insecureSkipVerify": { + "type": "boolean" + }, + "timeout": { + "description": "Timeout is the time to wait for a server's response. Its a duration e.g 15s, 2h30m.\nMissing value or empty string or means no timeout.", + "type": "string" + }, + "url": { + "type": "string" + } + } + } + } + }, + "httpLoadBalancer": { + "type": "object", + "required": [ + "address", + "path", + "port" + ], + "properties": { + "address": { + "type": "string" + }, + "collectorName": { + "type": "string" + }, + "exclude": { + "oneOf": [{"type": "string"},{"type": "boolean"}] + }, + "path": { + "type": "string" + }, + "port": { + "type": "integer" + }, + "timeout": { + "type": "string" + } + } + }, + "ipv4Interfaces": { + "type": "object", + "properties": { + "collectorName": { + "type": "string" + }, + "exclude": { + "oneOf": [{"type": "string"},{"type": "boolean"}] + } + } + }, + "journald": { + "type": "object", + "properties": { + "collectorName": { + "type": "string" + }, + "dmesg": { + "type": "boolean" + }, + "exclude": { + "oneOf": [{"type": "string"},{"type": "boolean"}] + }, + "lines": { + "type": "integer" + }, + "output": { + "type": "string" + }, + "reverse": { + "type": "boolean" + }, + "since": { + "type": "string" + }, + "system": { + "type": "boolean" + }, + "timeout": { + "type": "string" + }, + "units": { + "type": "array", + "items": { + "type": "string" + } + }, + "until": { + "type": "string" + }, + "utc": { + "type": "boolean" + } + } + }, + "kernelConfigs": { + "type": "object", + "properties": { + "collectorName": { + "type": "string" + }, + "exclude": { + "oneOf": [{"type": "string"},{"type": "boolean"}] + } + } + }, + "kernelModules": { + "type": "object", + "properties": { + "collectorName": { + "type": "string" + }, + "exclude": { + "oneOf": [{"type": "string"},{"type": "boolean"}] + } + } + }, + "kubernetes": { + "type": "object", + "properties": { + "collectorName": { + "type": "string" + }, + "exclude": { + "oneOf": [{"type": "string"},{"type": "boolean"}] + } + } + }, + "memory": { + "type": "object", + "properties": { + "collectorName": { + "type": "string" + }, + "exclude": { + "oneOf": [{"type": "string"},{"type": "boolean"}] + } + } + }, + "run": { + "type": "object", + "required": [ + "args", + "command" + ], + "properties": { + "args": { + "type": "array", + "items": { + "type": "string" + } + }, + "collectorName": { + "type": "string" + }, + "command": { + "type": "string" + }, + "env": { + "type": "array", + "items": { + "type": "string" + } + }, + "exclude": { + "oneOf": [{"type": "string"},{"type": "boolean"}] + }, + "ignoreParentEnvs": { + "type": "boolean" + }, + "inheritEnvs": { + "type": "array", + "items": { + "type": "string" + } + }, + "input": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "outputDir": { + "type": "string" + }, + "timeout": { + "type": "string" + } + } + }, + "subnetAvailable": { + "type": "object", + "required": [ + "CIDRRangeAlloc", + "desiredCIDR" + ], + "properties": { + "CIDRRangeAlloc": { + "type": "string" + }, + "collectorName": { + "type": "string" + }, + "desiredCIDR": { + "type": "integer" + }, + "exclude": { + "oneOf": [{"type": "string"},{"type": "boolean"}] + } + } + }, + "systemPackages": { + "type": "object", + "properties": { + "amzn": { + "type": "array", + "items": { + "type": "string" + } + }, + "amzn2": { + "type": "array", + "items": { + "type": "string" + } + }, + "centos": { + "type": "array", + "items": { + "type": "string" + } + }, + "centos7": { + "type": "array", + "items": { + "type": "string" + } + }, + "centos8": { + "type": "array", + "items": { + "type": "string" + } + }, + "centos9": { + "type": "array", + "items": { + "type": "string" + } + }, + "collectorName": { + "type": "string" + }, + "exclude": { + "oneOf": [{"type": "string"},{"type": "boolean"}] + }, + "ol": { + "type": "array", + "items": { + "type": "string" + } + }, + "ol7": { + "type": "array", + "items": { + "type": "string" + } + }, + "ol8": { + "type": "array", + "items": { + "type": "string" + } + }, + "ol9": { + "type": "array", + "items": { + "type": "string" + } + }, + "rhel": { + "type": "array", + "items": { + "type": "string" + } + }, + "rhel7": { + "type": "array", + "items": { + "type": "string" + } + }, + "rhel8": { + "type": "array", + "items": { + "type": "string" + } + }, + "rhel9": { + "type": "array", + "items": { + "type": "string" + } + }, + "rocky": { + "type": "array", + "items": { + "type": "string" + } + }, + "rocky8": { + "type": "array", + "items": { + "type": "string" + } + }, + "rocky9": { + "type": "array", + "items": { + "type": "string" + } + }, + "ubuntu": { + "type": "array", + "items": { + "type": "string" + } + }, + "ubuntu16": { + "type": "array", + "items": { + "type": "string" + } + }, + "ubuntu18": { + "type": "array", + "items": { + "type": "string" + } + }, + "ubuntu20": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "tcpConnect": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "string" + }, + "collectorName": { + "type": "string" + }, + "exclude": { + "oneOf": [{"type": "string"},{"type": "boolean"}] + }, + "timeout": { + "type": "string" + } + } + }, + "tcpLoadBalancer": { + "type": "object", + "required": [ + "address", + "port" + ], + "properties": { + "address": { + "type": "string" + }, + "collectorName": { + "type": "string" + }, + "exclude": { + "oneOf": [{"type": "string"},{"type": "boolean"}] + }, + "port": { + "type": "integer" + }, + "timeout": { + "type": "string" + } + } + }, + "tcpPortStatus": { + "type": "object", + "required": [ + "port" + ], + "properties": { + "collectorName": { + "type": "string" + }, + "exclude": { + "oneOf": [{"type": "string"},{"type": "boolean"}] + }, + "interface": { + "type": "string" + }, + "port": { + "type": "integer" + } + } + }, + "time": { + "type": "object", + "properties": { + "collectorName": { + "type": "string" + }, + "exclude": { + "oneOf": [{"type": "string"},{"type": "boolean"}] + } + } + }, + "udpPortStatus": { + "type": "object", + "required": [ + "port" + ], + "properties": { + "collectorName": { + "type": "string" + }, + "exclude": { + "oneOf": [{"type": "string"},{"type": "boolean"}] + }, + "interface": { + "type": "string" + }, + "port": { + "type": "integer" + } + } + } + } + } + }, "uri": { "type": "string" } From 0fb0a07e5522af81be837dd5b44191763bf188e7 Mon Sep 17 00:00:00 2001 From: Ash <159829404+hedge-sparrow@users.noreply.github.com> Date: Tue, 22 Oct 2024 15:06:17 +0100 Subject: [PATCH 16/25] [collect] accept stdin (#1657) --- cmd/collect/cli/run.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/cmd/collect/cli/run.go b/cmd/collect/cli/run.go index 7daf02206..42771b671 100644 --- a/cmd/collect/cli/run.go +++ b/cmd/collect/cli/run.go @@ -2,6 +2,7 @@ package cli import ( "fmt" + "io" "io/ioutil" "net/http" "os" @@ -49,6 +50,13 @@ func runCollect(v *viper.Viper, arg string) error { } collectorContent = spec + } else if arg == "-" { + b, err := io.ReadAll(os.Stdin) + if err != nil { + return err + } + + collectorContent = b } else if _, err = os.Stat(arg); err == nil { b, err := os.ReadFile(arg) if err != nil { From c968fca125de8413acec0dabfbb9218780db378d Mon Sep 17 00:00:00 2001 From: Ash <159829404+hedge-sparrow@users.noreply.github.com> Date: Tue, 22 Oct 2024 17:06:07 +0100 Subject: [PATCH 17/25] Allow `collect` to chroot itself (#1658) * Enable chroot * typo * platform specific chroot functions * Add friendly chroot warning if running without elevated permissions --- cmd/collect/cli/chroot_darwin.go | 21 +++++++++++++++++++++ cmd/collect/cli/chroot_linux.go | 21 +++++++++++++++++++++ cmd/collect/cli/chroot_windows.go | 9 +++++++++ cmd/collect/cli/root.go | 5 +++++ 4 files changed, 56 insertions(+) create mode 100644 cmd/collect/cli/chroot_darwin.go create mode 100644 cmd/collect/cli/chroot_linux.go create mode 100644 cmd/collect/cli/chroot_windows.go diff --git a/cmd/collect/cli/chroot_darwin.go b/cmd/collect/cli/chroot_darwin.go new file mode 100644 index 000000000..c24bf1120 --- /dev/null +++ b/cmd/collect/cli/chroot_darwin.go @@ -0,0 +1,21 @@ +package cli + +import ( + "errors" + "syscall" + + "github.com/replicatedhq/troubleshoot/internal/util" +) + +func checkAndSetChroot(newroot string) error { + if newroot == "" { + return nil + } + if !util.IsRunningAsRoot() { + return errors.New("Can only chroot when run as root") + } + if err := syscall.Chroot(newroot); err != nil { + return err + } + return nil +} diff --git a/cmd/collect/cli/chroot_linux.go b/cmd/collect/cli/chroot_linux.go new file mode 100644 index 000000000..c24bf1120 --- /dev/null +++ b/cmd/collect/cli/chroot_linux.go @@ -0,0 +1,21 @@ +package cli + +import ( + "errors" + "syscall" + + "github.com/replicatedhq/troubleshoot/internal/util" +) + +func checkAndSetChroot(newroot string) error { + if newroot == "" { + return nil + } + if !util.IsRunningAsRoot() { + return errors.New("Can only chroot when run as root") + } + if err := syscall.Chroot(newroot); err != nil { + return err + } + return nil +} diff --git a/cmd/collect/cli/chroot_windows.go b/cmd/collect/cli/chroot_windows.go new file mode 100644 index 000000000..84b349a20 --- /dev/null +++ b/cmd/collect/cli/chroot_windows.go @@ -0,0 +1,9 @@ +package cli + +import ( + "errors" +) + +func checkAndSetChroot(newroot string) error { + return errors.New("chroot is only implimented in linux/darwin") +} diff --git a/cmd/collect/cli/root.go b/cmd/collect/cli/root.go index dd3b27627..86fdc5c63 100644 --- a/cmd/collect/cli/root.go +++ b/cmd/collect/cli/root.go @@ -32,6 +32,10 @@ func RootCmd() *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { v := viper.GetViper() + if err := checkAndSetChroot(v.GetString("chroot")); err != nil { + return err + } + return runCollect(v, args[0]) }, PostRun: func(cmd *cobra.Command, args []string) { @@ -53,6 +57,7 @@ func RootCmd() *cobra.Command { cmd.Flags().String("selector", "", "selector (label query) to filter remote collection nodes on.") cmd.Flags().Bool("collect-without-permissions", false, "always generate a support bundle, even if it some require additional permissions") cmd.Flags().Bool("debug", false, "enable debug logging") + cmd.Flags().String("chroot", "", "Chroot to path") // hidden in favor of the `insecure-skip-tls-verify` flag cmd.Flags().Bool("allow-insecure-connections", false, "when set, do not verify TLS certs when retrieving spec and reporting results") From af8f682244b6bb7a7c5a590e2501b1abb2218075 Mon Sep 17 00:00:00 2001 From: Emmanuel Ferdman Date: Wed, 23 Oct 2024 06:52:47 +0300 Subject: [PATCH 18/25] fix: update feature epic proposal template (#1654) Signed-off-by: Emmanuel Ferdman --- .github/ISSUE_TEMPLATE/feature_epic.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/feature_epic.md b/.github/ISSUE_TEMPLATE/feature_epic.md index ae2179621..370c37311 100644 --- a/.github/ISSUE_TEMPLATE/feature_epic.md +++ b/.github/ISSUE_TEMPLATE/feature_epic.md @@ -7,7 +7,7 @@ labels: 'epic' # Design Proposal -Link to the [proposal](https://github.com/replicatedhq/troubleshoot/tree/main/design/template.md) +Link to the [proposal](https://github.com/replicatedhq/troubleshoot/tree/main/docs/design/template.md) # Definition of done From 7ed2f4bff216be4098dce773e49637c075aabebe Mon Sep 17 00:00:00 2001 From: Ricardo Maraschini Date: Wed, 23 Oct 2024 12:43:56 +0200 Subject: [PATCH 19/25] feat: Handle failure to load cluster specs gracefully in support-bundle command (#1660) * feat: Handle failure to load cluster specs gracefully in support-bundle command In some scenarios, we don't want to fail when unable to load specs from the cluster. This is particularly useful when: - A host support bundle is available on disk. - There are specs defined in the cluster. - The cluster is malfunctioning or inaccessible. - We still need to generate a support bundle using only the host specs. - This change allows users to generate a support bundle even if the embedded cluster is not functioning properly, making the process more resilient. The primary motivation is to introduce a new command: ``` ./embedded-cluster support-bundle ``` When executed, this command attempts to collect both host and cluster specs. However, if the embedded cluster is broken or unavailable, the command will skip loading the cluster specs and focus on the host, ensuring that users can still gather critical information without interruption. * chore: use fmt printf instead of klog * Update internal/specs/specs.go Co-authored-by: Evans Mungai --------- Co-authored-by: Evans Mungai --- internal/specs/specs.go | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/internal/specs/specs.go b/internal/specs/specs.go index cfb0060f0..80af7737d 100644 --- a/internal/specs/specs.go +++ b/internal/specs/specs.go @@ -221,10 +221,14 @@ func LoadFromCLIArgs(ctx context.Context, client kubernetes.Interface, args []st if vp.GetBool("load-cluster-specs") { clusterKinds, err := LoadFromCluster(ctx, client, vp.GetStringSlice("selector"), vp.GetString("namespace")) if err != nil { - return nil, types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, err) + if kinds.IsEmpty() { + return nil, types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, err) + } + // TODO: Consider colour coding and graceful failures when loading specs + fmt.Printf("failed to load specs from the cluster: %v\n", err) + } else { + kinds.Add(clusterKinds) } - - kinds.Add(clusterKinds) } return kinds, nil From eacff7112fdae8652ec5d2e4acb67afe1d053fac Mon Sep 17 00:00:00 2001 From: ada mancini Date: Wed, 23 Oct 2024 18:15:08 -0400 Subject: [PATCH 20/25] support adding a CA cert to http collector (#1624) * add a TLS parameter for cacert * pass a ca cert into http request * test preflight * make schemas * log extra information from http request * pass a proxy into the collector spec * hitting a segfault; breakpoint * accept a dir, file, or a string-literal as CA * move tls params into get, put, post methods * test for cert untrusted response * make generate * make schemas * more test cases * make schemas * dont include system certs * make generate && make schemas * resolve gosec G402 warning * remove old check for system certs * ignore errcheck "return value not checked" linter errors --- .golangci.yaml | 2 + config/crds/troubleshoot.sh_collectors.yaml | 138 ++++++++++++ .../crds/troubleshoot.sh_hostcollectors.yaml | 69 ++++++ .../crds/troubleshoot.sh_hostpreflights.yaml | 138 ++++++++++++ config/crds/troubleshoot.sh_preflights.yaml | 138 ++++++++++++ .../troubleshoot.sh_remotecollectors.yaml | 69 ++++++ .../crds/troubleshoot.sh_supportbundles.yaml | 138 ++++++++++++ go.mod | 2 +- .../troubleshoot/v1beta2/collector_shared.go | 12 +- .../v1beta2/zz_generated.deepcopy.go | 15 ++ pkg/collect/host_http.go | 6 +- pkg/collect/http.go | 117 ++++++++-- pkg/collect/http_test.go | 38 +++- schemas/collector-troubleshoot-v1beta2.json | 210 ++++++++++++++++++ schemas/preflight-troubleshoot-v1beta2.json | 210 ++++++++++++++++++ .../supportbundle-troubleshoot-v1beta2.json | 210 ++++++++++++++++++ testdata/https-proxy-replicated-app.yaml | 74 ++++++ 17 files changed, 1561 insertions(+), 25 deletions(-) create mode 100644 testdata/https-proxy-replicated-app.yaml diff --git a/.golangci.yaml b/.golangci.yaml index a80b6564c..9dcd6b279 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -11,3 +11,5 @@ linters: - gofmt - gosec - govet + disable: + - errcheck diff --git a/config/crds/troubleshoot.sh_collectors.yaml b/config/crds/troubleshoot.sh_collectors.yaml index d30a32fc2..ed5222823 100644 --- a/config/crds/troubleshoot.sh_collectors.yaml +++ b/config/crds/troubleshoot.sh_collectors.yaml @@ -408,11 +408,34 @@ spec: type: object insecureSkipVerify: type: boolean + proxy: + type: string timeout: description: |- Timeout is the time to wait for a server's response. Its a duration e.g 15s, 2h30m. Missing value or empty string or means no timeout. type: string + tls: + properties: + cacert: + type: string + clientCert: + type: string + clientKey: + type: string + secret: + properties: + name: + type: string + namespace: + type: string + required: + - name + - namespace + type: object + skipVerify: + type: boolean + type: object url: type: string required: @@ -430,11 +453,34 @@ spec: type: object insecureSkipVerify: type: boolean + proxy: + type: string timeout: description: |- Timeout is the time to wait for a server's response. Its a duration e.g 15s, 2h30m. Missing value or empty string or means no timeout. type: string + tls: + properties: + cacert: + type: string + clientCert: + type: string + clientKey: + type: string + secret: + properties: + name: + type: string + namespace: + type: string + required: + - name + - namespace + type: object + skipVerify: + type: boolean + type: object url: type: string required: @@ -450,11 +496,34 @@ spec: type: object insecureSkipVerify: type: boolean + proxy: + type: string timeout: description: |- Timeout is the time to wait for a server's response. Its a duration e.g 15s, 2h30m. Missing value or empty string or means no timeout. type: string + tls: + properties: + cacert: + type: string + clientCert: + type: string + clientKey: + type: string + secret: + properties: + name: + type: string + namespace: + type: string + required: + - name + - namespace + type: object + skipVerify: + type: boolean + type: object url: type: string required: @@ -17054,11 +17123,34 @@ spec: type: object insecureSkipVerify: type: boolean + proxy: + type: string timeout: description: |- Timeout is the time to wait for a server's response. Its a duration e.g 15s, 2h30m. Missing value or empty string or means no timeout. type: string + tls: + properties: + cacert: + type: string + clientCert: + type: string + clientKey: + type: string + secret: + properties: + name: + type: string + namespace: + type: string + required: + - name + - namespace + type: object + skipVerify: + type: boolean + type: object url: type: string required: @@ -17074,11 +17166,34 @@ spec: type: object insecureSkipVerify: type: boolean + proxy: + type: string timeout: description: |- Timeout is the time to wait for a server's response. Its a duration e.g 15s, 2h30m. Missing value or empty string or means no timeout. type: string + tls: + properties: + cacert: + type: string + clientCert: + type: string + clientKey: + type: string + secret: + properties: + name: + type: string + namespace: + type: string + required: + - name + - namespace + type: object + skipVerify: + type: boolean + type: object url: type: string required: @@ -17094,11 +17209,34 @@ spec: type: object insecureSkipVerify: type: boolean + proxy: + type: string timeout: description: |- Timeout is the time to wait for a server's response. Its a duration e.g 15s, 2h30m. Missing value or empty string or means no timeout. type: string + tls: + properties: + cacert: + type: string + clientCert: + type: string + clientKey: + type: string + secret: + properties: + name: + type: string + namespace: + type: string + required: + - name + - namespace + type: object + skipVerify: + type: boolean + type: object url: type: string required: diff --git a/config/crds/troubleshoot.sh_hostcollectors.yaml b/config/crds/troubleshoot.sh_hostcollectors.yaml index 61b6907f1..14414a0b3 100644 --- a/config/crds/troubleshoot.sh_hostcollectors.yaml +++ b/config/crds/troubleshoot.sh_hostcollectors.yaml @@ -1400,11 +1400,34 @@ spec: type: object insecureSkipVerify: type: boolean + proxy: + type: string timeout: description: |- Timeout is the time to wait for a server's response. Its a duration e.g 15s, 2h30m. Missing value or empty string or means no timeout. type: string + tls: + properties: + cacert: + type: string + clientCert: + type: string + clientKey: + type: string + secret: + properties: + name: + type: string + namespace: + type: string + required: + - name + - namespace + type: object + skipVerify: + type: boolean + type: object url: type: string required: @@ -1420,11 +1443,34 @@ spec: type: object insecureSkipVerify: type: boolean + proxy: + type: string timeout: description: |- Timeout is the time to wait for a server's response. Its a duration e.g 15s, 2h30m. Missing value or empty string or means no timeout. type: string + tls: + properties: + cacert: + type: string + clientCert: + type: string + clientKey: + type: string + secret: + properties: + name: + type: string + namespace: + type: string + required: + - name + - namespace + type: object + skipVerify: + type: boolean + type: object url: type: string required: @@ -1440,11 +1486,34 @@ spec: type: object insecureSkipVerify: type: boolean + proxy: + type: string timeout: description: |- Timeout is the time to wait for a server's response. Its a duration e.g 15s, 2h30m. Missing value or empty string or means no timeout. type: string + tls: + properties: + cacert: + type: string + clientCert: + type: string + clientKey: + type: string + secret: + properties: + name: + type: string + namespace: + type: string + required: + - name + - namespace + type: object + skipVerify: + type: boolean + type: object url: type: string required: diff --git a/config/crds/troubleshoot.sh_hostpreflights.yaml b/config/crds/troubleshoot.sh_hostpreflights.yaml index f8c06613c..1b1bf828b 100644 --- a/config/crds/troubleshoot.sh_hostpreflights.yaml +++ b/config/crds/troubleshoot.sh_hostpreflights.yaml @@ -1400,11 +1400,34 @@ spec: type: object insecureSkipVerify: type: boolean + proxy: + type: string timeout: description: |- Timeout is the time to wait for a server's response. Its a duration e.g 15s, 2h30m. Missing value or empty string or means no timeout. type: string + tls: + properties: + cacert: + type: string + clientCert: + type: string + clientKey: + type: string + secret: + properties: + name: + type: string + namespace: + type: string + required: + - name + - namespace + type: object + skipVerify: + type: boolean + type: object url: type: string required: @@ -1420,11 +1443,34 @@ spec: type: object insecureSkipVerify: type: boolean + proxy: + type: string timeout: description: |- Timeout is the time to wait for a server's response. Its a duration e.g 15s, 2h30m. Missing value or empty string or means no timeout. type: string + tls: + properties: + cacert: + type: string + clientCert: + type: string + clientKey: + type: string + secret: + properties: + name: + type: string + namespace: + type: string + required: + - name + - namespace + type: object + skipVerify: + type: boolean + type: object url: type: string required: @@ -1440,11 +1486,34 @@ spec: type: object insecureSkipVerify: type: boolean + proxy: + type: string timeout: description: |- Timeout is the time to wait for a server's response. Its a duration e.g 15s, 2h30m. Missing value or empty string or means no timeout. type: string + tls: + properties: + cacert: + type: string + clientCert: + type: string + clientKey: + type: string + secret: + properties: + name: + type: string + namespace: + type: string + required: + - name + - namespace + type: object + skipVerify: + type: boolean + type: object url: type: string required: @@ -1888,11 +1957,34 @@ spec: type: object insecureSkipVerify: type: boolean + proxy: + type: string timeout: description: |- Timeout is the time to wait for a server's response. Its a duration e.g 15s, 2h30m. Missing value or empty string or means no timeout. type: string + tls: + properties: + cacert: + type: string + clientCert: + type: string + clientKey: + type: string + secret: + properties: + name: + type: string + namespace: + type: string + required: + - name + - namespace + type: object + skipVerify: + type: boolean + type: object url: type: string required: @@ -1908,11 +2000,34 @@ spec: type: object insecureSkipVerify: type: boolean + proxy: + type: string timeout: description: |- Timeout is the time to wait for a server's response. Its a duration e.g 15s, 2h30m. Missing value or empty string or means no timeout. type: string + tls: + properties: + cacert: + type: string + clientCert: + type: string + clientKey: + type: string + secret: + properties: + name: + type: string + namespace: + type: string + required: + - name + - namespace + type: object + skipVerify: + type: boolean + type: object url: type: string required: @@ -1928,11 +2043,34 @@ spec: type: object insecureSkipVerify: type: boolean + proxy: + type: string timeout: description: |- Timeout is the time to wait for a server's response. Its a duration e.g 15s, 2h30m. Missing value or empty string or means no timeout. type: string + tls: + properties: + cacert: + type: string + clientCert: + type: string + clientKey: + type: string + secret: + properties: + name: + type: string + namespace: + type: string + required: + - name + - namespace + type: object + skipVerify: + type: boolean + type: object url: type: string required: diff --git a/config/crds/troubleshoot.sh_preflights.yaml b/config/crds/troubleshoot.sh_preflights.yaml index d762ce2c6..27eef2914 100644 --- a/config/crds/troubleshoot.sh_preflights.yaml +++ b/config/crds/troubleshoot.sh_preflights.yaml @@ -2137,11 +2137,34 @@ spec: type: object insecureSkipVerify: type: boolean + proxy: + type: string timeout: description: |- Timeout is the time to wait for a server's response. Its a duration e.g 15s, 2h30m. Missing value or empty string or means no timeout. type: string + tls: + properties: + cacert: + type: string + clientCert: + type: string + clientKey: + type: string + secret: + properties: + name: + type: string + namespace: + type: string + required: + - name + - namespace + type: object + skipVerify: + type: boolean + type: object url: type: string required: @@ -2159,11 +2182,34 @@ spec: type: object insecureSkipVerify: type: boolean + proxy: + type: string timeout: description: |- Timeout is the time to wait for a server's response. Its a duration e.g 15s, 2h30m. Missing value or empty string or means no timeout. type: string + tls: + properties: + cacert: + type: string + clientCert: + type: string + clientKey: + type: string + secret: + properties: + name: + type: string + namespace: + type: string + required: + - name + - namespace + type: object + skipVerify: + type: boolean + type: object url: type: string required: @@ -2179,11 +2225,34 @@ spec: type: object insecureSkipVerify: type: boolean + proxy: + type: string timeout: description: |- Timeout is the time to wait for a server's response. Its a duration e.g 15s, 2h30m. Missing value or empty string or means no timeout. type: string + tls: + properties: + cacert: + type: string + clientCert: + type: string + clientKey: + type: string + secret: + properties: + name: + type: string + namespace: + type: string + required: + - name + - namespace + type: object + skipVerify: + type: boolean + type: object url: type: string required: @@ -18737,11 +18806,34 @@ spec: type: object insecureSkipVerify: type: boolean + proxy: + type: string timeout: description: |- Timeout is the time to wait for a server's response. Its a duration e.g 15s, 2h30m. Missing value or empty string or means no timeout. type: string + tls: + properties: + cacert: + type: string + clientCert: + type: string + clientKey: + type: string + secret: + properties: + name: + type: string + namespace: + type: string + required: + - name + - namespace + type: object + skipVerify: + type: boolean + type: object url: type: string required: @@ -18757,11 +18849,34 @@ spec: type: object insecureSkipVerify: type: boolean + proxy: + type: string timeout: description: |- Timeout is the time to wait for a server's response. Its a duration e.g 15s, 2h30m. Missing value or empty string or means no timeout. type: string + tls: + properties: + cacert: + type: string + clientCert: + type: string + clientKey: + type: string + secret: + properties: + name: + type: string + namespace: + type: string + required: + - name + - namespace + type: object + skipVerify: + type: boolean + type: object url: type: string required: @@ -18777,11 +18892,34 @@ spec: type: object insecureSkipVerify: type: boolean + proxy: + type: string timeout: description: |- Timeout is the time to wait for a server's response. Its a duration e.g 15s, 2h30m. Missing value or empty string or means no timeout. type: string + tls: + properties: + cacert: + type: string + clientCert: + type: string + clientKey: + type: string + secret: + properties: + name: + type: string + namespace: + type: string + required: + - name + - namespace + type: object + skipVerify: + type: boolean + type: object url: type: string required: diff --git a/config/crds/troubleshoot.sh_remotecollectors.yaml b/config/crds/troubleshoot.sh_remotecollectors.yaml index e408a2c18..c4b8765cc 100644 --- a/config/crds/troubleshoot.sh_remotecollectors.yaml +++ b/config/crds/troubleshoot.sh_remotecollectors.yaml @@ -221,11 +221,34 @@ spec: type: object insecureSkipVerify: type: boolean + proxy: + type: string timeout: description: |- Timeout is the time to wait for a server's response. Its a duration e.g 15s, 2h30m. Missing value or empty string or means no timeout. type: string + tls: + properties: + cacert: + type: string + clientCert: + type: string + clientKey: + type: string + secret: + properties: + name: + type: string + namespace: + type: string + required: + - name + - namespace + type: object + skipVerify: + type: boolean + type: object url: type: string required: @@ -241,11 +264,34 @@ spec: type: object insecureSkipVerify: type: boolean + proxy: + type: string timeout: description: |- Timeout is the time to wait for a server's response. Its a duration e.g 15s, 2h30m. Missing value or empty string or means no timeout. type: string + tls: + properties: + cacert: + type: string + clientCert: + type: string + clientKey: + type: string + secret: + properties: + name: + type: string + namespace: + type: string + required: + - name + - namespace + type: object + skipVerify: + type: boolean + type: object url: type: string required: @@ -261,11 +307,34 @@ spec: type: object insecureSkipVerify: type: boolean + proxy: + type: string timeout: description: |- Timeout is the time to wait for a server's response. Its a duration e.g 15s, 2h30m. Missing value or empty string or means no timeout. type: string + tls: + properties: + cacert: + type: string + clientCert: + type: string + clientKey: + type: string + secret: + properties: + name: + type: string + namespace: + type: string + required: + - name + - namespace + type: object + skipVerify: + type: boolean + type: object url: type: string required: diff --git a/config/crds/troubleshoot.sh_supportbundles.yaml b/config/crds/troubleshoot.sh_supportbundles.yaml index 220bd1c33..2c8c5c2ed 100644 --- a/config/crds/troubleshoot.sh_supportbundles.yaml +++ b/config/crds/troubleshoot.sh_supportbundles.yaml @@ -2168,11 +2168,34 @@ spec: type: object insecureSkipVerify: type: boolean + proxy: + type: string timeout: description: |- Timeout is the time to wait for a server's response. Its a duration e.g 15s, 2h30m. Missing value or empty string or means no timeout. type: string + tls: + properties: + cacert: + type: string + clientCert: + type: string + clientKey: + type: string + secret: + properties: + name: + type: string + namespace: + type: string + required: + - name + - namespace + type: object + skipVerify: + type: boolean + type: object url: type: string required: @@ -2190,11 +2213,34 @@ spec: type: object insecureSkipVerify: type: boolean + proxy: + type: string timeout: description: |- Timeout is the time to wait for a server's response. Its a duration e.g 15s, 2h30m. Missing value or empty string or means no timeout. type: string + tls: + properties: + cacert: + type: string + clientCert: + type: string + clientKey: + type: string + secret: + properties: + name: + type: string + namespace: + type: string + required: + - name + - namespace + type: object + skipVerify: + type: boolean + type: object url: type: string required: @@ -2210,11 +2256,34 @@ spec: type: object insecureSkipVerify: type: boolean + proxy: + type: string timeout: description: |- Timeout is the time to wait for a server's response. Its a duration e.g 15s, 2h30m. Missing value or empty string or means no timeout. type: string + tls: + properties: + cacert: + type: string + clientCert: + type: string + clientKey: + type: string + secret: + properties: + name: + type: string + namespace: + type: string + required: + - name + - namespace + type: object + skipVerify: + type: boolean + type: object url: type: string required: @@ -19978,11 +20047,34 @@ spec: type: object insecureSkipVerify: type: boolean + proxy: + type: string timeout: description: |- Timeout is the time to wait for a server's response. Its a duration e.g 15s, 2h30m. Missing value or empty string or means no timeout. type: string + tls: + properties: + cacert: + type: string + clientCert: + type: string + clientKey: + type: string + secret: + properties: + name: + type: string + namespace: + type: string + required: + - name + - namespace + type: object + skipVerify: + type: boolean + type: object url: type: string required: @@ -19998,11 +20090,34 @@ spec: type: object insecureSkipVerify: type: boolean + proxy: + type: string timeout: description: |- Timeout is the time to wait for a server's response. Its a duration e.g 15s, 2h30m. Missing value or empty string or means no timeout. type: string + tls: + properties: + cacert: + type: string + clientCert: + type: string + clientKey: + type: string + secret: + properties: + name: + type: string + namespace: + type: string + required: + - name + - namespace + type: object + skipVerify: + type: boolean + type: object url: type: string required: @@ -20018,11 +20133,34 @@ spec: type: object insecureSkipVerify: type: boolean + proxy: + type: string timeout: description: |- Timeout is the time to wait for a server's response. Its a duration e.g 15s, 2h30m. Missing value or empty string or means no timeout. type: string + tls: + properties: + cacert: + type: string + clientCert: + type: string + clientKey: + type: string + secret: + properties: + name: + type: string + namespace: + type: string + required: + - name + - namespace + type: object + skipVerify: + type: boolean + type: object url: type: string required: diff --git a/go.mod b/go.mod index c504a25cf..409f7ff63 100644 --- a/go.mod +++ b/go.mod @@ -41,7 +41,6 @@ require ( github.com/vmware-tanzu/velero v1.14.1 go.opentelemetry.io/otel v1.31.0 go.opentelemetry.io/otel/sdk v1.31.0 - go.opentelemetry.io/otel/trace v1.31.0 golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 golang.org/x/mod v0.21.0 golang.org/x/sync v0.8.0 @@ -124,6 +123,7 @@ require ( go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect go.opentelemetry.io/otel/metric v1.31.0 // indirect + go.opentelemetry.io/otel/trace v1.31.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/tools v0.22.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 // indirect diff --git a/pkg/apis/troubleshoot/v1beta2/collector_shared.go b/pkg/apis/troubleshoot/v1beta2/collector_shared.go index 1cc212f52..554f732e4 100644 --- a/pkg/apis/troubleshoot/v1beta2/collector_shared.go +++ b/pkg/apis/troubleshoot/v1beta2/collector_shared.go @@ -186,7 +186,9 @@ type Get struct { Headers map[string]string `json:"headers,omitempty" yaml:"headers,omitempty"` // Timeout is the time to wait for a server's response. Its a duration e.g 15s, 2h30m. // Missing value or empty string or means no timeout. - Timeout string `json:"timeout,omitempty" yaml:"timeout,omitempty"` + Timeout string `json:"timeout,omitempty" yaml:"timeout,omitempty"` + TLS *TLSParams `json:"tls,omitempty" yaml:"tls,omitempty"` + Proxy string `json:"proxy,omitempty" yaml:"proxy,omitempty"` } type Post struct { @@ -196,7 +198,9 @@ type Post struct { Body string `json:"body,omitempty" yaml:"body,omitempty"` // Timeout is the time to wait for a server's response. Its a duration e.g 15s, 2h30m. // Missing value or empty string or means no timeout. - Timeout string `json:"timeout,omitempty" yaml:"timeout,omitempty"` + Timeout string `json:"timeout,omitempty" yaml:"timeout,omitempty"` + TLS *TLSParams `json:"tls,omitempty" yaml:"tls,omitempty"` + Proxy string `json:"proxy,omitempty" yaml:"proxy,omitempty"` } type Put struct { @@ -206,7 +210,9 @@ type Put struct { Body string `json:"body,omitempty" yaml:"body,omitempty"` // Timeout is the time to wait for a server's response. Its a duration e.g 15s, 2h30m. // Missing value or empty string or means no timeout. - Timeout string `json:"timeout,omitempty" yaml:"timeout,omitempty"` + Timeout string `json:"timeout,omitempty" yaml:"timeout,omitempty"` + TLS *TLSParams `json:"tls,omitempty" yaml:"tls,omitempty"` + Proxy string `json:"proxy,omitempty" yaml:"proxy,omitempty"` } type Database struct { diff --git a/pkg/apis/troubleshoot/v1beta2/zz_generated.deepcopy.go b/pkg/apis/troubleshoot/v1beta2/zz_generated.deepcopy.go index af210443e..8fb1ac586 100644 --- a/pkg/apis/troubleshoot/v1beta2/zz_generated.deepcopy.go +++ b/pkg/apis/troubleshoot/v1beta2/zz_generated.deepcopy.go @@ -1620,6 +1620,11 @@ func (in *Get) DeepCopyInto(out *Get) { (*out)[key] = val } } + if in.TLS != nil { + in, out := &in.TLS, &out.TLS + *out = new(TLSParams) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Get. @@ -3416,6 +3421,11 @@ func (in *Post) DeepCopyInto(out *Post) { (*out)[key] = val } } + if in.TLS != nil { + in, out := &in.TLS, &out.TLS + *out = new(TLSParams) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Post. @@ -3560,6 +3570,11 @@ func (in *Put) DeepCopyInto(out *Put) { (*out)[key] = val } } + if in.TLS != nil { + in, out := &in.TLS, &out.TLS + *out = new(TLSParams) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Put. diff --git a/pkg/collect/host_http.go b/pkg/collect/host_http.go index 06e9685d0..6bf2b194c 100644 --- a/pkg/collect/host_http.go +++ b/pkg/collect/host_http.go @@ -32,15 +32,15 @@ func (c *CollectHostHTTP) Collect(progressChan chan<- interface{}) (map[string][ case httpCollector.Get != nil: response, err = doRequest( "GET", httpCollector.Get.URL, httpCollector.Get.Headers, - "", httpCollector.Get.InsecureSkipVerify, httpCollector.Get.Timeout) + "", httpCollector.Get.InsecureSkipVerify, httpCollector.Get.Timeout, httpCollector.Get.TLS, httpCollector.Get.Proxy) case httpCollector.Post != nil: response, err = doRequest( "POST", httpCollector.Post.URL, httpCollector.Post.Headers, - httpCollector.Post.Body, httpCollector.Post.InsecureSkipVerify, httpCollector.Post.Timeout) + httpCollector.Post.Body, httpCollector.Post.InsecureSkipVerify, httpCollector.Post.Timeout, httpCollector.Post.TLS, httpCollector.Post.Proxy) case httpCollector.Put != nil: response, err = doRequest( "PUT", httpCollector.Put.URL, httpCollector.Put.Headers, - httpCollector.Put.Body, httpCollector.Put.InsecureSkipVerify, httpCollector.Put.Timeout) + httpCollector.Put.Body, httpCollector.Put.InsecureSkipVerify, httpCollector.Put.Timeout, httpCollector.Put.TLS, httpCollector.Put.Proxy) default: return nil, errors.New("no supported http request type") } diff --git a/pkg/collect/http.go b/pkg/collect/http.go index 697c30127..05fcd93a4 100644 --- a/pkg/collect/http.go +++ b/pkg/collect/http.go @@ -3,9 +3,13 @@ package collect import ( "bytes" "crypto/tls" + "crypto/x509" "encoding/json" "io" "net/http" + "net/http/httputil" + neturl "net/url" + "os" "path/filepath" "strings" "time" @@ -52,16 +56,13 @@ func (c *CollectHTTP) Collect(progressChan chan<- interface{}) (CollectorResult, switch { case c.Collector.Get != nil: response, err = doRequest( - "GET", c.Collector.Get.URL, c.Collector.Get.Headers, - "", c.Collector.Get.InsecureSkipVerify, c.Collector.Get.Timeout) + "GET", c.Collector.Get.URL, c.Collector.Get.Headers, "", c.Collector.Get.InsecureSkipVerify, c.Collector.Get.Timeout, c.Collector.Get.TLS, c.Collector.Get.Proxy) case c.Collector.Post != nil: response, err = doRequest( - "POST", c.Collector.Post.URL, c.Collector.Post.Headers, - c.Collector.Post.Body, c.Collector.Post.InsecureSkipVerify, c.Collector.Post.Timeout) + "POST", c.Collector.Post.URL, c.Collector.Post.Headers, c.Collector.Post.Body, c.Collector.Post.InsecureSkipVerify, c.Collector.Post.Timeout, c.Collector.Post.TLS, c.Collector.Post.Proxy) case c.Collector.Put != nil: response, err = doRequest( - "PUT", c.Collector.Put.URL, c.Collector.Put.Headers, - c.Collector.Put.Body, c.Collector.Put.InsecureSkipVerify, c.Collector.Put.Timeout) + "PUT", c.Collector.Put.URL, c.Collector.Put.Headers, c.Collector.Put.Body, c.Collector.Put.InsecureSkipVerify, c.Collector.Put.Timeout, c.Collector.Put.TLS, c.Collector.Put.Proxy) default: return nil, errors.New("no supported http request type") } @@ -82,24 +83,74 @@ func (c *CollectHTTP) Collect(progressChan chan<- interface{}) (CollectorResult, return output, nil } -func doRequest(method, url string, headers map[string]string, body string, insecureSkipVerify bool, timeout string) (*http.Response, error) { +func handleFileOrDir(path string) (bool, error) { + f, err := os.Stat(path) + if err != nil { + klog.V(2).Infof("Failed to stat file path: %s\n", err) + return false, err + } + if f.IsDir() { + os.Setenv("SSL_CERT_DIR", path) + klog.V(2).Infof("Using SSL_CERT_DIR: %s\n", path) + } else if f.Mode().IsRegular() { + os.Setenv("SSL_CERT_FILE", path) + klog.V(2).Infof("Using SSL_CERT_FILE: %s\n", path) + } + return true, nil +} + +func isPEMCertificate(s string) bool { + return strings.Contains(s, "BEGIN CERTIFICATE") || strings.Contains(s, "BEGIN RSA PRIVATE KEY") +} + +func doRequest(method, url string, headers map[string]string, body string, insecureSkipVerify bool, timeout string, tlsParams *troubleshootv1beta2.TLSParams, proxy string) (*http.Response, error) { + t, err := parseTimeout(timeout) if err != nil { return nil, err } - httpClient := &http.Client{ - Timeout: t, + tlsConfig := &tls.Config{MinVersion: tls.VersionTLS12} + httpTransport := &http.Transport{} + + if tlsParams != nil && tlsParams.CACert != "" { + if isPEMCertificate(tlsParams.CACert) { + klog.V(2).Infof("Using PEM certificate from spec\n") + certPool := x509.NewCertPool() + if !certPool.AppendCertsFromPEM([]byte(tlsParams.CACert)) { + return nil, errors.New("failed to append certificate to cert pool") + } + tlsConfig.RootCAs = certPool + } else if _, err := handleFileOrDir(tlsParams.CACert); err != nil { + return nil, errors.Wrap(err, "failed to handle cacert file path") + } } if insecureSkipVerify { - httpClient.Transport = &http.Transport{ - TLSClientConfig: &tls.Config{ - InsecureSkipVerify: true, - }, + tlsConfig.InsecureSkipVerify = true + } + + httpTransport.TLSClientConfig = tlsConfig + + if proxy != "" || os.Getenv("HTTPS_PROXY") != "" { + if proxy != "" { + klog.V(2).Infof("Using proxy from spec: %s\n", proxy) + httpTransport.Proxy = func(req *http.Request) (*neturl.URL, error) { + return neturl.Parse(proxy) + } + } else { + klog.V(2).Infof("Using proxy from environment: %s\n", os.Getenv("HTTPS_PROXY")) + httpTransport.Proxy = http.ProxyFromEnvironment } } + httpClient := &http.Client{ + Timeout: t, + Transport: &LoggingTransport{ + Transport: httpTransport, + }, + } + req, err := http.NewRequest(method, url, strings.NewReader(body)) if err != nil { return nil, err @@ -112,6 +163,36 @@ func doRequest(method, url string, headers map[string]string, body string, insec return httpClient.Do(req) } +type LoggingTransport struct { + Transport http.RoundTripper +} + +func (t *LoggingTransport) RoundTrip(req *http.Request) (*http.Response, error) { + // Log the request + dumpReq, err := httputil.DumpRequestOut(req, true) + if err != nil { + klog.V(2).Infof("Failed to dump request: %+v\n", err) + } else { + klog.V(2).Infof("Request: %s\n", dumpReq) + } + + resp, err := t.Transport.RoundTrip(req) + + // Log the response + if err != nil { + klog.V(2).Infof("Request failed: %+v\n", err) + } else { + dumpResp, err := httputil.DumpResponse(resp, true) + if err != nil { + klog.V(2).Infof("Failed to dump response: %v+\n", err) + } else { + klog.V(2).Infof("Response: %s\n", dumpResp) + } + } + + return resp, err +} + func responseToOutput(response *http.Response, err error) ([]byte, error) { output := make(map[string]interface{}) if err != nil { @@ -130,11 +211,15 @@ func responseToOutput(response *http.Response, err error) ([]byte, error) { } var rawJSON json.RawMessage - if err := json.Unmarshal(body, &rawJSON); err != nil { - klog.Infof("failed to unmarshal response body as JSON: %v", err) + if len(body) > 0 { + if err := json.Unmarshal(body, &rawJSON); err != nil { + klog.Infof("failed to unmarshal response body as JSON: %+v", err) + rawJSON = json.RawMessage{} + } + } else { rawJSON = json.RawMessage{} + klog.V(2).Infof("empty response body\n") } - output["response"] = HTTPResponse{ Status: response.StatusCode, Body: string(body), diff --git a/pkg/collect/http_test.go b/pkg/collect/http_test.go index b558765a0..46fd216e5 100644 --- a/pkg/collect/http_test.go +++ b/pkg/collect/http_test.go @@ -83,6 +83,13 @@ func TestCollectHTTP_Collect(t *testing.T) { res.WriteHeader(http.StatusInternalServerError) res.Write([]byte("{\"error\": { \"message\": \"context deadline exceeded\"}}")) }) + mux.HandleFunc("/certificate-mismatch", func(res http.ResponseWriter, req *http.Request) { + time.Sleep(1 * time.Millisecond) + fmt.Println("Sleeping for 2 seconds on /error call") + res.Header().Set("Content-Type", "application/json; charset=utf-8") + res.WriteHeader(http.StatusInternalServerError) + res.Write([]byte("{\"error\": { \"message\": \"Request failed: proxyconnect tcp: tls: failed to verify certificate: x509: \"10.0.0.254\" certificate is not trusted\"}}")) + }) sample_get_response := &ResponseData{ Response: Response{ @@ -125,9 +132,15 @@ func TestCollectHTTP_Collect(t *testing.T) { Message: "context deadline exceeded", }, } - sample_error_bytes, _ := sample_error_response.ToJSONbytes() + sample_certificate_untrusted := &ErrorResponse{ + Error: HTTPError{ + Message: "Request failed: proxyconnect tcp: tls: failed to verify certificate: x509: \"10.0.0.254\" certificate is not trusted", + }, + } + sample_certificate_untrusted_bytes, _ := sample_certificate_untrusted.ToJSONbytes() + tests := []CollectorTest{ { // check valid file path when CollectorName is not supplied @@ -250,7 +263,25 @@ func TestCollectHTTP_Collect(t *testing.T) { checkTimeout: true, wantErr: false, }, - // TODO: add TLS cert case + { + name: "TLS: certificate is not trusted", + Collector: &troubleshootv1beta2.HTTP{ + CollectorMeta: troubleshootv1beta2.CollectorMeta{ + CollectorName: "", + }, + Get: &troubleshootv1beta2.Get{ + Timeout: "300ms", + }, + }, + args: args{ + progressChan: nil, + }, + want: CollectorResult{ + "result.json": sample_certificate_untrusted_bytes, + }, + checkTimeout: true, + wantErr: true, + }, } for _, tt := range tests { var ts *httptest.Server @@ -273,6 +304,9 @@ func TestCollectHTTP_Collect(t *testing.T) { c.Collector.Get.URL = fmt.Sprintf("%s%s", url, "/error") response_data := sample_error_response response_data.testCollectHTTP(t, &tt, c) + c.Collector.Get.URL = fmt.Sprintf("%s%s", url, "/certificate-mismatch") + response_data = sample_certificate_untrusted + response_data.testCollectHTTP(t, &tt, c) } else { c.Collector.Get.URL = fmt.Sprintf("%s%s", url, "/get") response_data := sample_get_response diff --git a/schemas/collector-troubleshoot-v1beta2.json b/schemas/collector-troubleshoot-v1beta2.json index c6b6105a8..d54ff65a6 100644 --- a/schemas/collector-troubleshoot-v1beta2.json +++ b/schemas/collector-troubleshoot-v1beta2.json @@ -570,10 +570,45 @@ "insecureSkipVerify": { "type": "boolean" }, + "proxy": { + "type": "string" + }, "timeout": { "description": "Timeout is the time to wait for a server's response. Its a duration e.g 15s, 2h30m.\nMissing value or empty string or means no timeout.", "type": "string" }, + "tls": { + "type": "object", + "properties": { + "cacert": { + "type": "string" + }, + "clientCert": { + "type": "string" + }, + "clientKey": { + "type": "string" + }, + "secret": { + "type": "object", + "required": [ + "name", + "namespace" + ], + "properties": { + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + } + } + }, + "skipVerify": { + "type": "boolean" + } + } + }, "url": { "type": "string" } @@ -600,10 +635,45 @@ "insecureSkipVerify": { "type": "boolean" }, + "proxy": { + "type": "string" + }, "timeout": { "description": "Timeout is the time to wait for a server's response. Its a duration e.g 15s, 2h30m.\nMissing value or empty string or means no timeout.", "type": "string" }, + "tls": { + "type": "object", + "properties": { + "cacert": { + "type": "string" + }, + "clientCert": { + "type": "string" + }, + "clientKey": { + "type": "string" + }, + "secret": { + "type": "object", + "required": [ + "name", + "namespace" + ], + "properties": { + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + } + } + }, + "skipVerify": { + "type": "boolean" + } + } + }, "url": { "type": "string" } @@ -627,10 +697,45 @@ "insecureSkipVerify": { "type": "boolean" }, + "proxy": { + "type": "string" + }, "timeout": { "description": "Timeout is the time to wait for a server's response. Its a duration e.g 15s, 2h30m.\nMissing value or empty string or means no timeout.", "type": "string" }, + "tls": { + "type": "object", + "properties": { + "cacert": { + "type": "string" + }, + "clientCert": { + "type": "string" + }, + "clientKey": { + "type": "string" + }, + "secret": { + "type": "object", + "required": [ + "name", + "namespace" + ], + "properties": { + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + } + } + }, + "skipVerify": { + "type": "boolean" + } + } + }, "url": { "type": "string" } @@ -14622,10 +14727,45 @@ "insecureSkipVerify": { "type": "boolean" }, + "proxy": { + "type": "string" + }, "timeout": { "description": "Timeout is the time to wait for a server's response. Its a duration e.g 15s, 2h30m.\nMissing value or empty string or means no timeout.", "type": "string" }, + "tls": { + "type": "object", + "properties": { + "cacert": { + "type": "string" + }, + "clientCert": { + "type": "string" + }, + "clientKey": { + "type": "string" + }, + "secret": { + "type": "object", + "required": [ + "name", + "namespace" + ], + "properties": { + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + } + } + }, + "skipVerify": { + "type": "boolean" + } + } + }, "url": { "type": "string" } @@ -14649,10 +14789,45 @@ "insecureSkipVerify": { "type": "boolean" }, + "proxy": { + "type": "string" + }, "timeout": { "description": "Timeout is the time to wait for a server's response. Its a duration e.g 15s, 2h30m.\nMissing value or empty string or means no timeout.", "type": "string" }, + "tls": { + "type": "object", + "properties": { + "cacert": { + "type": "string" + }, + "clientCert": { + "type": "string" + }, + "clientKey": { + "type": "string" + }, + "secret": { + "type": "object", + "required": [ + "name", + "namespace" + ], + "properties": { + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + } + } + }, + "skipVerify": { + "type": "boolean" + } + } + }, "url": { "type": "string" } @@ -14676,10 +14851,45 @@ "insecureSkipVerify": { "type": "boolean" }, + "proxy": { + "type": "string" + }, "timeout": { "description": "Timeout is the time to wait for a server's response. Its a duration e.g 15s, 2h30m.\nMissing value or empty string or means no timeout.", "type": "string" }, + "tls": { + "type": "object", + "properties": { + "cacert": { + "type": "string" + }, + "clientCert": { + "type": "string" + }, + "clientKey": { + "type": "string" + }, + "secret": { + "type": "object", + "required": [ + "name", + "namespace" + ], + "properties": { + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + } + } + }, + "skipVerify": { + "type": "boolean" + } + } + }, "url": { "type": "string" } diff --git a/schemas/preflight-troubleshoot-v1beta2.json b/schemas/preflight-troubleshoot-v1beta2.json index 7b4ddd0f7..02e7687e8 100644 --- a/schemas/preflight-troubleshoot-v1beta2.json +++ b/schemas/preflight-troubleshoot-v1beta2.json @@ -3234,10 +3234,45 @@ "insecureSkipVerify": { "type": "boolean" }, + "proxy": { + "type": "string" + }, "timeout": { "description": "Timeout is the time to wait for a server's response. Its a duration e.g 15s, 2h30m.\nMissing value or empty string or means no timeout.", "type": "string" }, + "tls": { + "type": "object", + "properties": { + "cacert": { + "type": "string" + }, + "clientCert": { + "type": "string" + }, + "clientKey": { + "type": "string" + }, + "secret": { + "type": "object", + "required": [ + "name", + "namespace" + ], + "properties": { + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + } + } + }, + "skipVerify": { + "type": "boolean" + } + } + }, "url": { "type": "string" } @@ -3264,10 +3299,45 @@ "insecureSkipVerify": { "type": "boolean" }, + "proxy": { + "type": "string" + }, "timeout": { "description": "Timeout is the time to wait for a server's response. Its a duration e.g 15s, 2h30m.\nMissing value or empty string or means no timeout.", "type": "string" }, + "tls": { + "type": "object", + "properties": { + "cacert": { + "type": "string" + }, + "clientCert": { + "type": "string" + }, + "clientKey": { + "type": "string" + }, + "secret": { + "type": "object", + "required": [ + "name", + "namespace" + ], + "properties": { + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + } + } + }, + "skipVerify": { + "type": "boolean" + } + } + }, "url": { "type": "string" } @@ -3291,10 +3361,45 @@ "insecureSkipVerify": { "type": "boolean" }, + "proxy": { + "type": "string" + }, "timeout": { "description": "Timeout is the time to wait for a server's response. Its a duration e.g 15s, 2h30m.\nMissing value or empty string or means no timeout.", "type": "string" }, + "tls": { + "type": "object", + "properties": { + "cacert": { + "type": "string" + }, + "clientCert": { + "type": "string" + }, + "clientKey": { + "type": "string" + }, + "secret": { + "type": "object", + "required": [ + "name", + "namespace" + ], + "properties": { + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + } + } + }, + "skipVerify": { + "type": "boolean" + } + } + }, "url": { "type": "string" } @@ -17215,10 +17320,45 @@ "insecureSkipVerify": { "type": "boolean" }, + "proxy": { + "type": "string" + }, "timeout": { "description": "Timeout is the time to wait for a server's response. Its a duration e.g 15s, 2h30m.\nMissing value or empty string or means no timeout.", "type": "string" }, + "tls": { + "type": "object", + "properties": { + "cacert": { + "type": "string" + }, + "clientCert": { + "type": "string" + }, + "clientKey": { + "type": "string" + }, + "secret": { + "type": "object", + "required": [ + "name", + "namespace" + ], + "properties": { + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + } + } + }, + "skipVerify": { + "type": "boolean" + } + } + }, "url": { "type": "string" } @@ -17242,10 +17382,45 @@ "insecureSkipVerify": { "type": "boolean" }, + "proxy": { + "type": "string" + }, "timeout": { "description": "Timeout is the time to wait for a server's response. Its a duration e.g 15s, 2h30m.\nMissing value or empty string or means no timeout.", "type": "string" }, + "tls": { + "type": "object", + "properties": { + "cacert": { + "type": "string" + }, + "clientCert": { + "type": "string" + }, + "clientKey": { + "type": "string" + }, + "secret": { + "type": "object", + "required": [ + "name", + "namespace" + ], + "properties": { + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + } + } + }, + "skipVerify": { + "type": "boolean" + } + } + }, "url": { "type": "string" } @@ -17269,10 +17444,45 @@ "insecureSkipVerify": { "type": "boolean" }, + "proxy": { + "type": "string" + }, "timeout": { "description": "Timeout is the time to wait for a server's response. Its a duration e.g 15s, 2h30m.\nMissing value or empty string or means no timeout.", "type": "string" }, + "tls": { + "type": "object", + "properties": { + "cacert": { + "type": "string" + }, + "clientCert": { + "type": "string" + }, + "clientKey": { + "type": "string" + }, + "secret": { + "type": "object", + "required": [ + "name", + "namespace" + ], + "properties": { + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + } + } + }, + "skipVerify": { + "type": "boolean" + } + } + }, "url": { "type": "string" } diff --git a/schemas/supportbundle-troubleshoot-v1beta2.json b/schemas/supportbundle-troubleshoot-v1beta2.json index 884ac10f6..2c1ac4ff7 100644 --- a/schemas/supportbundle-troubleshoot-v1beta2.json +++ b/schemas/supportbundle-troubleshoot-v1beta2.json @@ -3280,10 +3280,45 @@ "insecureSkipVerify": { "type": "boolean" }, + "proxy": { + "type": "string" + }, "timeout": { "description": "Timeout is the time to wait for a server's response. Its a duration e.g 15s, 2h30m.\nMissing value or empty string or means no timeout.", "type": "string" }, + "tls": { + "type": "object", + "properties": { + "cacert": { + "type": "string" + }, + "clientCert": { + "type": "string" + }, + "clientKey": { + "type": "string" + }, + "secret": { + "type": "object", + "required": [ + "name", + "namespace" + ], + "properties": { + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + } + } + }, + "skipVerify": { + "type": "boolean" + } + } + }, "url": { "type": "string" } @@ -3310,10 +3345,45 @@ "insecureSkipVerify": { "type": "boolean" }, + "proxy": { + "type": "string" + }, "timeout": { "description": "Timeout is the time to wait for a server's response. Its a duration e.g 15s, 2h30m.\nMissing value or empty string or means no timeout.", "type": "string" }, + "tls": { + "type": "object", + "properties": { + "cacert": { + "type": "string" + }, + "clientCert": { + "type": "string" + }, + "clientKey": { + "type": "string" + }, + "secret": { + "type": "object", + "required": [ + "name", + "namespace" + ], + "properties": { + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + } + } + }, + "skipVerify": { + "type": "boolean" + } + } + }, "url": { "type": "string" } @@ -3337,10 +3407,45 @@ "insecureSkipVerify": { "type": "boolean" }, + "proxy": { + "type": "string" + }, "timeout": { "description": "Timeout is the time to wait for a server's response. Its a duration e.g 15s, 2h30m.\nMissing value or empty string or means no timeout.", "type": "string" }, + "tls": { + "type": "object", + "properties": { + "cacert": { + "type": "string" + }, + "clientCert": { + "type": "string" + }, + "clientKey": { + "type": "string" + }, + "secret": { + "type": "object", + "required": [ + "name", + "namespace" + ], + "properties": { + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + } + } + }, + "skipVerify": { + "type": "boolean" + } + } + }, "url": { "type": "string" } @@ -19134,10 +19239,45 @@ "insecureSkipVerify": { "type": "boolean" }, + "proxy": { + "type": "string" + }, "timeout": { "description": "Timeout is the time to wait for a server's response. Its a duration e.g 15s, 2h30m.\nMissing value or empty string or means no timeout.", "type": "string" }, + "tls": { + "type": "object", + "properties": { + "cacert": { + "type": "string" + }, + "clientCert": { + "type": "string" + }, + "clientKey": { + "type": "string" + }, + "secret": { + "type": "object", + "required": [ + "name", + "namespace" + ], + "properties": { + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + } + } + }, + "skipVerify": { + "type": "boolean" + } + } + }, "url": { "type": "string" } @@ -19161,10 +19301,45 @@ "insecureSkipVerify": { "type": "boolean" }, + "proxy": { + "type": "string" + }, "timeout": { "description": "Timeout is the time to wait for a server's response. Its a duration e.g 15s, 2h30m.\nMissing value or empty string or means no timeout.", "type": "string" }, + "tls": { + "type": "object", + "properties": { + "cacert": { + "type": "string" + }, + "clientCert": { + "type": "string" + }, + "clientKey": { + "type": "string" + }, + "secret": { + "type": "object", + "required": [ + "name", + "namespace" + ], + "properties": { + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + } + } + }, + "skipVerify": { + "type": "boolean" + } + } + }, "url": { "type": "string" } @@ -19188,10 +19363,45 @@ "insecureSkipVerify": { "type": "boolean" }, + "proxy": { + "type": "string" + }, "timeout": { "description": "Timeout is the time to wait for a server's response. Its a duration e.g 15s, 2h30m.\nMissing value or empty string or means no timeout.", "type": "string" }, + "tls": { + "type": "object", + "properties": { + "cacert": { + "type": "string" + }, + "clientCert": { + "type": "string" + }, + "clientKey": { + "type": "string" + }, + "secret": { + "type": "object", + "required": [ + "name", + "namespace" + ], + "properties": { + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + } + } + }, + "skipVerify": { + "type": "boolean" + } + } + }, "url": { "type": "string" } diff --git a/testdata/https-proxy-replicated-app.yaml b/testdata/https-proxy-replicated-app.yaml new file mode 100644 index 000000000..675c1001c --- /dev/null +++ b/testdata/https-proxy-replicated-app.yaml @@ -0,0 +1,74 @@ +apiVersion: troubleshoot.sh/v1beta2 +kind: HostPreflight +metadata: + name: mitm-proxy +spec: + collectors: + - http: + collectorName: &https https + get: + url: &url https://replicated.app + tls: + cacert: &ca |- + -----BEGIN CERTIFICATE----- + MIIDajCCAlKgAwIBAgIUFlUns1qeD6ss4cdXz52287KtPQswDQYJKoZIhvcNAQEL + BQAwTjELMAkGA1UEBhMCVVMxDjAMBgNVBAgMBVN0YXRlMQ0wCwYDVQQHDARDaXR5 + MRMwEQYDVQQKDApSZXBsaWNhdGVkMQswCQYDVQQLDAJJVDAeFw0yNDA5MjcxNTU5 + MDJaFw0yNDEwMDQxNTU5MDJaME4xCzAJBgNVBAYTAlVTMQ4wDAYDVQQIDAVTdGF0 + ZTENMAsGA1UEBwwEQ2l0eTETMBEGA1UECgwKUmVwbGljYXRlZDELMAkGA1UECwwC + SVQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDLe7PlgdPiApQzZzkY + 0dN/NDBib72Y5TAEhNguPVDY1Rj4PLiKjUXvHbVsRLpP8DrKJAeK/kJqeR4xr1O5 + dQenCoTZTHX24TLFx0D1SKfdUtpxzKta8jd+O5TwaY/tLsi9YcJ6mz8n7+giJH1r + ZH5Isa9JkZ3fb2+VoX054I/C88MfsvdZahL7/RHLvolRiLeV7X86Zx2EJ3hUFWoZ + kYeIggbt2BeikeDlHQDBmxzpIaP1IMl3LHOjZhj7TiNuSYtDiE8OQIV34c9IZ1yi + lcUjrwKQCfzaE9lZK5UbS3KRD1XFSrSP4tWVsUmesYeFD+nc5/wku/J+PXDM1QAu + B8q9AgMBAAGjQDA+MA8GA1UdEQQIMAaHBAoKHskwDAYDVR0TBAUwAwEB/zAdBgNV + HQ4EFgQUuwXZYrzbdQVGCS5O0sdlCJo761cwDQYJKoZIhvcNAQELBQADggEBAH3G + 9C6sJ+uR9ZAOnFyCQEdBVaw02NMOY0ajc8gMrmgl9btx1rLnS8r+zLf9Jev0YxiG + Pq6HbkceQNa6Rl6l6JH4O0sV0KUXe5r7kPPYv9pMsy+JZYH9H1ppUr0a13s4vrgA + 4YbFE3TispC6WXFng4w85ODc9nmXGDvjPX6mzZxcsxooDX5+PPAo+WueKutOZMvT + yvB2hUgb4hy6CT6OvJJFb9Lh1Hl5aE/9FKgF3u/Tq2U3SSzMHMZiWzUVfAO0J1Ev + jcr8Mb5t3iQwH3t2eT07K2fouPa70vbOfj1kSiexUoUllHgoOXUeOpGv4Aykly7m + C/XdeJyP1tnZ3j2ozPo= + -----END CERTIFICATE----- + - http: + collectorName: &https-proxy https-proxy + get: + url: *url + tls: + cacert: *ca + proxy: &proxy https://10.10.30.201:3130 + - http: + collectorName: &https-proxy-nocert https-proxy-nocert + get: + url: *url + proxy: *proxy + + analyzers: + - http: + checkName: *https-proxy + collectorName: *https-proxy + outcomes: + - pass: + when: &200 "statusCode == 200" + message: &passed checking https://replicated.app passed + - fail: + message: &failed checking https://replicated.app failed + - http: + checkName: *https-proxy-nocert + collectorName: *https-proxy-nocert + outcomes: + - pass: + when: *200 + message: *passed + - fail: + message: *failed + - http: + checkName: *https + collectorName: *https + outcomes: + - pass: + when: *200 + message: *passed + - fail: + message: *failed From 350418c6e93e60791ed499700dfe10b9ad3723a7 Mon Sep 17 00:00:00 2001 From: Dexter Yan Date: Fri, 25 Oct 2024 15:34:09 +1300 Subject: [PATCH 21/25] feat(host-collector): add progress for host collector (#1659) --- pkg/supportbundle/collect.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/pkg/supportbundle/collect.go b/pkg/supportbundle/collect.go index ec8e680c2..87ff2fcfb 100644 --- a/pkg/supportbundle/collect.go +++ b/pkg/supportbundle/collect.go @@ -227,14 +227,16 @@ func collectRemoteHost(ctx context.Context, collectSpecs []*troubleshootv1beta2. isExcluded, _ := collector.IsExcluded() if isExcluded { - opts.ProgressChan <- fmt.Sprintf("[%s] Excluding host collector", collector.Title()) + msg := fmt.Sprintf("[%s] Excluding host collector", collector.Title()) + opts.CollectorProgressCallback(opts.ProgressChan, msg) span.SetAttributes(attribute.Bool(constants.EXCLUDED, true)) span.End() continue } // Send progress event: starting the collector - opts.ProgressChan <- fmt.Sprintf("[%s] Running host collector...", collector.Title()) + msg := fmt.Sprintf("[%s] Running host collector...", collector.Title()) + opts.CollectorProgressCallback(opts.ProgressChan, msg) // Parameters for remote collection params := &collect.RemoteCollectParams{ @@ -255,12 +257,14 @@ func collectRemoteHost(ctx context.Context, collectSpecs []*troubleshootv1beta2. result, err := collect.RemoteHostCollect(ctx, *params) if err != nil { span.SetStatus(codes.Error, err.Error()) - opts.ProgressChan <- fmt.Sprintf("[%s] Error: %v", collector.Title(), err) + msg = fmt.Sprintf("[%s] Error: %v", collector.Title(), err) + opts.CollectorProgressCallback(opts.ProgressChan, msg) return errors.Wrap(err, "failed to run remote host collector") } // Send progress event: completed successfully - opts.ProgressChan <- fmt.Sprintf("[%s] Completed host collector", collector.Title()) + msg = fmt.Sprintf("[%s] Completed host collector", collector.Title()) + opts.CollectorProgressCallback(opts.ProgressChan, msg) // Aggregate the results for k, v := range result { From deda4ce98c38d5a6cafdbdc442493c753cfa0bc1 Mon Sep 17 00:00:00 2001 From: Evans Mungai Date: Fri, 25 Oct 2024 07:03:16 -0500 Subject: [PATCH 22/25] feat: Do not prompt users to save support bundle analysis results (#1662) In interactive mode, do not prompt users to save support bundle analysis results. Users end up providing this file instead of the support bundle archive. The analysis results are contained in the support bundle archive already Signed-off-by: Evans Mungai --- cmd/troubleshoot/cli/interactive_results.go | 133 +++----------------- pkg/preflight/interactive_results.go | 2 +- 2 files changed, 20 insertions(+), 115 deletions(-) diff --git a/cmd/troubleshoot/cli/interactive_results.go b/cmd/troubleshoot/cli/interactive_results.go index e623ab5db..4f5939b37 100644 --- a/cmd/troubleshoot/cli/interactive_results.go +++ b/cmd/troubleshoot/cli/interactive_results.go @@ -2,11 +2,6 @@ package cli import ( "fmt" - "io/ioutil" - "os" - "path" - "strings" - "time" "github.com/mitchellh/go-wordwrap" "github.com/pkg/errors" @@ -20,7 +15,6 @@ import ( var ( selectedResult = 0 table = widgets.NewTable() - isShowingSaved = false ) func showInteractiveResults(supportBundleName string, analyzeResults []*analyzerunner.AnalyzeResult, archivePath string) error { @@ -28,7 +22,7 @@ func showInteractiveResults(supportBundleName string, analyzeResults []*analyzer return errors.Wrap(err, "failed to create terminal ui") } defer ui.Close() - drawUI(supportBundleName, analyzeResults) + drawUI(supportBundleName, analyzeResults, archivePath) uiEvents := ui.PollEvents() for { @@ -38,29 +32,10 @@ func showInteractiveResults(supportBundleName string, analyzeResults []*analyzer case "": return nil case "q": - if isShowingSaved == true { - isShowingSaved = false - ui.Clear() - drawUI(supportBundleName, analyzeResults) - } else { - return nil - } - case "s": - filename, err := save(analyzeResults) - if err != nil { - // show - } else { - showSaved(filename, archivePath) - go func() { - time.Sleep(time.Second * 5) - isShowingSaved = false - ui.Clear() - drawUI(supportBundleName, analyzeResults) - }() - } + return nil case "": ui.Clear() - drawUI(supportBundleName, analyzeResults) + drawUI(supportBundleName, analyzeResults, archivePath) case "": if selectedResult < len(analyzeResults)-1 { selectedResult++ @@ -70,7 +45,7 @@ func showInteractiveResults(supportBundleName string, analyzeResults []*analyzer } table.ScrollDown() ui.Clear() - drawUI(supportBundleName, analyzeResults) + drawUI(supportBundleName, analyzeResults, archivePath) case "": if selectedResult > 0 { selectedResult-- @@ -80,16 +55,16 @@ func showInteractiveResults(supportBundleName string, analyzeResults []*analyzer } table.ScrollUp() ui.Clear() - drawUI(supportBundleName, analyzeResults) + drawUI(supportBundleName, analyzeResults, archivePath) } } } } -func drawUI(supportBundleName string, analyzeResults []*analyzerunner.AnalyzeResult) { +func drawUI(supportBundleName string, analyzeResults []*analyzerunner.AnalyzeResult, archivePath string) { drawGrid(analyzeResults) drawHeader(supportBundleName) - drawFooter() + drawFooter(archivePath) } func drawGrid(analyzeResults []*analyzerunner.AnalyzeResult) { @@ -102,9 +77,7 @@ func drawHeader(supportBundleName string) { title := widgets.NewParagraph() title.Text = fmt.Sprintf("%s Support Bundle Analysis", util.AppName(supportBundleName)) - title.TextStyle.Fg = ui.ColorWhite - title.TextStyle.Bg = ui.ColorClear - title.TextStyle.Modifier = ui.ModifierBold + title.TextStyle = ui.NewStyle(ui.ColorWhite, ui.ColorClear, ui.ModifierBold) title.Border = false left := termWidth/2 - 2*len(title.Text)/3 @@ -114,11 +87,18 @@ func drawHeader(supportBundleName string) { ui.Render(title) } -func drawFooter() { +func drawFooter(archivePath string) { termWidth, termHeight := ui.TerminalDimensions() + archivePathMsg := widgets.NewParagraph() + archivePathMsg.Text = "Support bundle archive: " + archivePath + archivePathMsg.Border = false + archivePathMsg.TextStyle = ui.NewStyle(ui.ColorWhite, ui.ColorClear, ui.ModifierBold) + + archivePathMsg.SetRect(0, termHeight-3, termWidth, termHeight-2) + instructions := widgets.NewParagraph() - instructions.Text = "[q] quit [s] save [↑][↓] scroll" + instructions.Text = "[q] quit [↑][↓] scroll" instructions.Border = false left := 0 @@ -127,13 +107,13 @@ func drawFooter() { bottom := termHeight instructions.SetRect(left, top, right, bottom) - ui.Render(instructions) + ui.Render(archivePathMsg, instructions) } func drawAnalyzersTable(analyzeResults []*analyzerunner.AnalyzeResult) { termWidth, termHeight := ui.TerminalDimensions() - table.SetRect(0, 3, termWidth/2, termHeight-6) + table.SetRect(0, 3, termWidth/2, termHeight-4) table.FillRow = true table.Border = true table.Rows = [][]string{} @@ -214,78 +194,3 @@ func drawDetails(analysisResult *analyzerunner.AnalyzeResult) { message.SetRect(termWidth/2, currentTop, termWidth, currentTop+height) ui.Render(message) } - -func showSaved(filename string, archivePath string) { - termWidth, termHeight := ui.TerminalDimensions() - - f := `A support bundle was generated and saved at %s. -Please send this file to your software vendor for support.` - additionalMessageText := fmt.Sprintf(f, archivePath) - - savedMessage := widgets.NewParagraph() - savedMessage.Text = fmt.Sprintf("Support Bundle analysis results saved to\n\n%s\n\n%s", filename, additionalMessageText) - savedMessage.WrapText = true - savedMessage.Border = true - - // Split the text into lines and find the longest line - lines := strings.Split(savedMessage.Text, "\n") - maxLineLength := 0 - for _, line := range lines { - if len(line) > maxLineLength { - // maxLineLength is set to half of the line length to prevent the showing text with more space than needed - maxLineLength = len(line)/2 + constants.MESSAGE_TEXT_PADDING - } - } - - if maxLineLength > termWidth/2 { - maxLineLength = termWidth / 2 - } - - left := termWidth/2 - maxLineLength - right := termWidth/2 + maxLineLength - top := termHeight/2 - len(lines) - bottom := termHeight/2 + len(lines) - - savedMessage.SetRect(left, top, right, bottom) - ui.Render(savedMessage) - - isShowingSaved = true -} - -func save(analyzeResults []*analyzerunner.AnalyzeResult) (string, error) { - filename := path.Join(util.HomeDir(), fmt.Sprintf("%s-results.txt", "support-bundle")) - _, err := os.Stat(filename) - if err == nil { - os.Remove(filename) - } - - results := "" - for _, analyzeResult := range analyzeResults { - result := "" - - if analyzeResult.IsPass { - result = "Check PASS\n" - } else if analyzeResult.IsWarn { - result = "Check WARN\n" - } else if analyzeResult.IsFail { - result = "Check FAIL\n" - } - - result = result + fmt.Sprintf("Title: %s\n", analyzeResult.Title) - result = result + fmt.Sprintf("Message: %s\n", analyzeResult.Message) - - if analyzeResult.URI != "" { - result = result + fmt.Sprintf("URI: %s\n", analyzeResult.URI) - } - - result = result + "\n------------\n" - - results = results + result - } - - if err := ioutil.WriteFile(filename, []byte(results), 0644); err != nil { - return "", errors.Wrap(err, "failed to save preflight results") - } - - return filename, nil -} diff --git a/pkg/preflight/interactive_results.go b/pkg/preflight/interactive_results.go index 43fda5870..d61c8c521 100644 --- a/pkg/preflight/interactive_results.go +++ b/pkg/preflight/interactive_results.go @@ -130,7 +130,7 @@ func drawFooter() { func drawPreflightTable(analyzeResults []*analyzerunner.AnalyzeResult) { termWidth, termHeight := ui.TerminalDimensions() - table.SetRect(0, 3, termWidth/2, termHeight-6) + table.SetRect(0, 3, termWidth/2, termHeight-4) table.FillRow = true table.Border = true table.Rows = [][]string{} From b33d5538e924820ae4f056220b9aed506f44d68f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 29 Oct 2024 06:08:07 +0000 Subject: [PATCH 23/25] chore(deps): bump the security group with 11 updates (#1665) Bumps the security group with 11 updates: | Package | From | To | | --- | --- | --- | | [github.com/fatih/color](https://github.com/fatih/color) | `1.17.0` | `1.18.0` | | [k8s.io/api](https://github.com/kubernetes/api) | `0.31.1` | `0.31.2` | | [k8s.io/apiextensions-apiserver](https://github.com/kubernetes/apiextensions-apiserver) | `0.31.1` | `0.31.2` | | [k8s.io/apimachinery](https://github.com/kubernetes/apimachinery) | `0.31.1` | `0.31.2` | | [k8s.io/apiserver](https://github.com/kubernetes/apiserver) | `0.31.1` | `0.31.2` | | [k8s.io/cli-runtime](https://github.com/kubernetes/cli-runtime) | `0.31.1` | `0.31.2` | | [k8s.io/client-go](https://github.com/kubernetes/client-go) | `0.31.1` | `0.31.2` | | [sigs.k8s.io/controller-runtime](https://github.com/kubernetes-sigs/controller-runtime) | `0.19.0` | `0.19.1` | | [sigs.k8s.io/e2e-framework](https://github.com/kubernetes-sigs/e2e-framework) | `0.4.0` | `0.5.0` | | [k8s.io/kubelet](https://github.com/kubernetes/kubelet) | `0.31.1` | `0.31.2` | | [k8s.io/metrics](https://github.com/kubernetes/metrics) | `0.31.1` | `0.31.2` | Updates `github.com/fatih/color` from 1.17.0 to 1.18.0 - [Release notes](https://github.com/fatih/color/releases) - [Commits](https://github.com/fatih/color/compare/v1.17.0...v1.18.0) Updates `k8s.io/api` from 0.31.1 to 0.31.2 - [Commits](https://github.com/kubernetes/api/compare/v0.31.1...v0.31.2) Updates `k8s.io/apiextensions-apiserver` from 0.31.1 to 0.31.2 - [Release notes](https://github.com/kubernetes/apiextensions-apiserver/releases) - [Commits](https://github.com/kubernetes/apiextensions-apiserver/compare/v0.31.1...v0.31.2) Updates `k8s.io/apimachinery` from 0.31.1 to 0.31.2 - [Commits](https://github.com/kubernetes/apimachinery/compare/v0.31.1...v0.31.2) Updates `k8s.io/apiserver` from 0.31.1 to 0.31.2 - [Commits](https://github.com/kubernetes/apiserver/compare/v0.31.1...v0.31.2) Updates `k8s.io/cli-runtime` from 0.31.1 to 0.31.2 - [Commits](https://github.com/kubernetes/cli-runtime/compare/v0.31.1...v0.31.2) Updates `k8s.io/client-go` from 0.31.1 to 0.31.2 - [Changelog](https://github.com/kubernetes/client-go/blob/master/CHANGELOG.md) - [Commits](https://github.com/kubernetes/client-go/compare/v0.31.1...v0.31.2) Updates `sigs.k8s.io/controller-runtime` from 0.19.0 to 0.19.1 - [Release notes](https://github.com/kubernetes-sigs/controller-runtime/releases) - [Changelog](https://github.com/kubernetes-sigs/controller-runtime/blob/main/RELEASE.md) - [Commits](https://github.com/kubernetes-sigs/controller-runtime/compare/v0.19.0...v0.19.1) Updates `sigs.k8s.io/e2e-framework` from 0.4.0 to 0.5.0 - [Release notes](https://github.com/kubernetes-sigs/e2e-framework/releases) - [Changelog](https://github.com/kubernetes-sigs/e2e-framework/blob/main/RELEASE.md) - [Commits](https://github.com/kubernetes-sigs/e2e-framework/compare/v0.4.0...v0.5.0) Updates `k8s.io/kubelet` from 0.31.1 to 0.31.2 - [Commits](https://github.com/kubernetes/kubelet/compare/v0.31.1...v0.31.2) Updates `k8s.io/metrics` from 0.31.1 to 0.31.2 - [Commits](https://github.com/kubernetes/metrics/compare/v0.31.1...v0.31.2) --- updated-dependencies: - dependency-name: github.com/fatih/color dependency-type: direct:production update-type: version-update:semver-minor dependency-group: security - dependency-name: k8s.io/api dependency-type: direct:production update-type: version-update:semver-patch dependency-group: security - dependency-name: k8s.io/apiextensions-apiserver dependency-type: direct:production update-type: version-update:semver-patch dependency-group: security - dependency-name: k8s.io/apimachinery dependency-type: direct:production update-type: version-update:semver-patch dependency-group: security - dependency-name: k8s.io/apiserver dependency-type: direct:production update-type: version-update:semver-patch dependency-group: security - dependency-name: k8s.io/cli-runtime dependency-type: direct:production update-type: version-update:semver-patch dependency-group: security - dependency-name: k8s.io/client-go dependency-type: direct:production update-type: version-update:semver-patch dependency-group: security - dependency-name: sigs.k8s.io/controller-runtime dependency-type: direct:production update-type: version-update:semver-patch dependency-group: security - dependency-name: sigs.k8s.io/e2e-framework dependency-type: direct:production update-type: version-update:semver-minor dependency-group: security - dependency-name: k8s.io/kubelet dependency-type: direct:production update-type: version-update:semver-patch dependency-group: security - dependency-name: k8s.io/metrics dependency-type: direct:production update-type: version-update:semver-patch dependency-group: security ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 26 +++++++++++++------------- go.sum | 52 ++++++++++++++++++++++++++-------------------------- 2 files changed, 39 insertions(+), 39 deletions(-) diff --git a/go.mod b/go.mod index 409f7ff63..077e6da1c 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/containerd/cgroups/v3 v3.0.3 github.com/containers/image/v5 v5.32.2 github.com/distribution/distribution/v3 v3.0.0-beta.1 - github.com/fatih/color v1.17.0 + github.com/fatih/color v1.18.0 github.com/go-logr/logr v1.4.2 github.com/go-redis/redis/v7 v7.4.1 github.com/go-sql-driver/mysql v1.8.1 @@ -45,16 +45,16 @@ require ( golang.org/x/mod v0.21.0 golang.org/x/sync v0.8.0 gopkg.in/yaml.v2 v2.4.0 - k8s.io/api v0.31.1 - k8s.io/apiextensions-apiserver v0.31.1 - k8s.io/apimachinery v0.31.1 - k8s.io/apiserver v0.31.1 - k8s.io/cli-runtime v0.31.1 - k8s.io/client-go v0.31.1 + k8s.io/api v0.31.2 + k8s.io/apiextensions-apiserver v0.31.2 + k8s.io/apimachinery v0.31.2 + k8s.io/apiserver v0.31.2 + k8s.io/cli-runtime v0.31.2 + k8s.io/client-go v0.31.2 k8s.io/klog/v2 v2.130.1 oras.land/oras-go v1.2.6 - sigs.k8s.io/controller-runtime v0.19.0 - sigs.k8s.io/e2e-framework v0.4.0 + sigs.k8s.io/controller-runtime v0.19.1 + sigs.k8s.io/e2e-framework v0.5.0 ) require ( @@ -115,7 +115,7 @@ require ( github.com/sourcegraph/conc v0.3.0 // indirect github.com/sylabs/sif/v2 v2.18.0 // indirect github.com/tchap/go-patricia/v2 v2.3.1 // indirect - github.com/vladimirvivien/gexe v0.2.0 // indirect + github.com/vladimirvivien/gexe v0.3.0 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect @@ -129,7 +129,7 @@ require ( google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect - k8s.io/component-base v0.31.1 // indirect + k8s.io/component-base v0.31.2 // indirect k8s.io/kubectl v0.31.1 // indirect ) @@ -254,8 +254,8 @@ require ( gopkg.in/yaml.v3 v3.0.1 // indirect helm.sh/helm/v3 v3.16.2 k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect - k8s.io/kubelet v0.31.1 - k8s.io/metrics v0.31.1 + k8s.io/kubelet v0.31.2 + k8s.io/metrics v0.31.2 k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 periph.io/x/host/v3 v3.8.2 sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect diff --git a/go.sum b/go.sum index 588d3c22d..45488eddd 100644 --- a/go.sum +++ b/go.sum @@ -365,8 +365,8 @@ github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d h1:105gxyaGwCFad8crR9dcMQWvV9Hvulu6hwUh4tWPJnM= github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d/go.mod h1:ZZMPRZwes7CROmyNKgQzC3XPs6L/G2EJLHddWejkmf4= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= -github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/foxcpp/go-mockdns v1.1.0 h1:jI0rD8M0wuYAxL7r/ynTrCQQq0BVqfB99Vgk7DlmewI= @@ -885,8 +885,8 @@ github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc= github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/vbatts/tar-split v0.11.5 h1:3bHCTIheBm1qFTcgh9oPu+nNBtX+XJIupG/vacinCts= github.com/vbatts/tar-split v0.11.5/go.mod h1:yZbwRsSeGjusneWgA781EKej9HF8vme8okylkAeNKLk= -github.com/vladimirvivien/gexe v0.2.0 h1:nbdAQ6vbZ+ZNsolCgSVb9Fno60kzSuvtzVh6Ytqi/xY= -github.com/vladimirvivien/gexe v0.2.0/go.mod h1:LHQL00w/7gDUKIak24n801ABp8C+ni6eBht9vGVst8w= +github.com/vladimirvivien/gexe v0.3.0 h1:4xwiOwGrDob5OMR6E92B9olDXYDglXdHhzR1ggYtWJM= +github.com/vladimirvivien/gexe v0.3.0/go.mod h1:fp7cy60ON1xjhtEI/+bfSEIXX35qgmI+iRYlGOqbBFM= github.com/vmware-tanzu/velero v1.14.1 h1:HYj73scn7ZqtfTanjW/X4W0Hn3w/qcfoRbrHCWM52iI= github.com/vmware-tanzu/velero v1.14.1/go.mod h1:/OzHzTvbevkkX+bK/BS4AgYMv6nKuOgsybuuvLWkSS0= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= @@ -1536,30 +1536,30 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -k8s.io/api v0.31.1 h1:Xe1hX/fPW3PXYYv8BlozYqw63ytA92snr96zMW9gWTU= -k8s.io/api v0.31.1/go.mod h1:sbN1g6eY6XVLeqNsZGLnI5FwVseTrZX7Fv3O26rhAaI= -k8s.io/apiextensions-apiserver v0.31.1 h1:L+hwULvXx+nvTYX/MKM3kKMZyei+UiSXQWciX/N6E40= -k8s.io/apiextensions-apiserver v0.31.1/go.mod h1:tWMPR3sgW+jsl2xm9v7lAyRF1rYEK71i9G5dRtkknoQ= -k8s.io/apimachinery v0.31.1 h1:mhcUBbj7KUjaVhyXILglcVjuS4nYXiwC+KKFBgIVy7U= -k8s.io/apimachinery v0.31.1/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= -k8s.io/apiserver v0.31.1 h1:Sars5ejQDCRBY5f7R3QFHdqN3s61nhkpaX8/k1iEw1c= -k8s.io/apiserver v0.31.1/go.mod h1:lzDhpeToamVZJmmFlaLwdYZwd7zB+WYRYIboqA1kGxM= -k8s.io/cli-runtime v0.31.1 h1:/ZmKhmZ6hNqDM+yf9s3Y4KEYakNXUn5sod2LWGGwCuk= -k8s.io/cli-runtime v0.31.1/go.mod h1:pKv1cDIaq7ehWGuXQ+A//1OIF+7DI+xudXtExMCbe9U= -k8s.io/client-go v0.31.1 h1:f0ugtWSbWpxHR7sjVpQwuvw9a3ZKLXX0u0itkFXufb0= -k8s.io/client-go v0.31.1/go.mod h1:sKI8871MJN2OyeqRlmA4W4KM9KBdBUpDLu/43eGemCg= -k8s.io/component-base v0.31.1 h1:UpOepcrX3rQ3ab5NB6g5iP0tvsgJWzxTyAo20sgYSy8= -k8s.io/component-base v0.31.1/go.mod h1:WGeaw7t/kTsqpVTaCoVEtillbqAhF2/JgvO0LDOMa0w= +k8s.io/api v0.31.2 h1:3wLBbL5Uom/8Zy98GRPXpJ254nEFpl+hwndmk9RwmL0= +k8s.io/api v0.31.2/go.mod h1:bWmGvrGPssSK1ljmLzd3pwCQ9MgoTsRCuK35u6SygUk= +k8s.io/apiextensions-apiserver v0.31.2 h1:W8EwUb8+WXBLu56ser5IudT2cOho0gAKeTOnywBLxd0= +k8s.io/apiextensions-apiserver v0.31.2/go.mod h1:i+Geh+nGCJEGiCGR3MlBDkS7koHIIKWVfWeRFiOsUcM= +k8s.io/apimachinery v0.31.2 h1:i4vUt2hPK56W6mlT7Ry+AO8eEsyxMD1U44NR22CLTYw= +k8s.io/apimachinery v0.31.2/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= +k8s.io/apiserver v0.31.2 h1:VUzOEUGRCDi6kX1OyQ801m4A7AUPglpsmGvdsekmcI4= +k8s.io/apiserver v0.31.2/go.mod h1:o3nKZR7lPlJqkU5I3Ove+Zx3JuoFjQobGX1Gctw6XuE= +k8s.io/cli-runtime v0.31.2 h1:7FQt4C4Xnqx8V1GJqymInK0FFsoC+fAZtbLqgXYVOLQ= +k8s.io/cli-runtime v0.31.2/go.mod h1:XROyicf+G7rQ6FQJMbeDV9jqxzkWXTYD6Uxd15noe0Q= +k8s.io/client-go v0.31.2 h1:Y2F4dxU5d3AQj+ybwSMqQnpZH9F30//1ObxOKlTI9yc= +k8s.io/client-go v0.31.2/go.mod h1:NPa74jSVR/+eez2dFsEIHNa+3o09vtNaWwWwb1qSxSs= +k8s.io/component-base v0.31.2 h1:Z1J1LIaC0AV+nzcPRFqfK09af6bZ4D1nAOpWsy9owlA= +k8s.io/component-base v0.31.2/go.mod h1:9PeyyFN/drHjtJZMCTkSpQJS3U9OXORnHQqMLDz0sUQ= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag= k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98= k8s.io/kubectl v0.31.1 h1:ih4JQJHxsEggFqDJEHSOdJ69ZxZftgeZvYo7M/cpp24= k8s.io/kubectl v0.31.1/go.mod h1:aNuQoR43W6MLAtXQ/Bu4GDmoHlbhHKuyD49lmTC8eJM= -k8s.io/kubelet v0.31.1 h1:aAxwVxGzbbMKKk/FnSjvkN52K3LdHhjhzmYcyGBuE0c= -k8s.io/kubelet v0.31.1/go.mod h1:8ZbexYHqUO946gXEfFmnMZiK2UKRGhk7LlGvJ71p2Ig= -k8s.io/metrics v0.31.1 h1:h4I4dakgh/zKflWYAOQhwf0EXaqy8LxAIyE/GBvxqRc= -k8s.io/metrics v0.31.1/go.mod h1:JuH1S9tJiH9q1VCY0yzSCawi7kzNLsDzlWDJN4xR+iA= +k8s.io/kubelet v0.31.2 h1:6Hytyw4LqWqhgzoi7sPfpDGClu2UfxmPmaiXPC4FRgI= +k8s.io/kubelet v0.31.2/go.mod h1:0E4++3cMWi2cJxOwuaQP3eMBa7PSOvAFgkTPlVc/2FA= +k8s.io/metrics v0.31.2 h1:sQhujR9m3HN/Nu/0fTfTscjnswQl0qkQAodEdGBS0N4= +k8s.io/metrics v0.31.2/go.mod h1:QqqyReApEWO1UEgXOSXiHCQod6yTxYctbAAQBWZkboU= k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A= k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= oras.land/oras-go v1.2.6 h1:z8cmxQXBU8yZ4mkytWqXfo6tZcamPwjsuxYU81xJ8Lk= @@ -1569,10 +1569,10 @@ periph.io/x/host/v3 v3.8.2/go.mod h1:yFL76AesNHR68PboofSWYaQTKmvPXsQH2Apvp/ls/K4 rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= -sigs.k8s.io/controller-runtime v0.19.0 h1:nWVM7aq+Il2ABxwiCizrVDSlmDcshi9llbaFbC0ji/Q= -sigs.k8s.io/controller-runtime v0.19.0/go.mod h1:iRmWllt8IlaLjvTTDLhRBXIEtkCK6hwVBJJsYS9Ajf4= -sigs.k8s.io/e2e-framework v0.4.0 h1:4yYmFDNNoTnazqmZJXQ6dlQF1vrnDbutmxlyvBpC5rY= -sigs.k8s.io/e2e-framework v0.4.0/go.mod h1:JilFQPF1OL1728ABhMlf9huse7h+uBJDXl9YeTs49A8= +sigs.k8s.io/controller-runtime v0.19.1 h1:Son+Q40+Be3QWb+niBXAg2vFiYWolDjjRfO8hn/cxOk= +sigs.k8s.io/controller-runtime v0.19.1/go.mod h1:iRmWllt8IlaLjvTTDLhRBXIEtkCK6hwVBJJsYS9Ajf4= +sigs.k8s.io/e2e-framework v0.5.0 h1:YLhk8R7EHuTFQAe6Fxy5eBzn5Vb+yamR5u8MH1Rq3cE= +sigs.k8s.io/e2e-framework v0.5.0/go.mod h1:jJSH8u2RNmruekUZgHAtmRjb5Wj67GErli9UjLSY7Zc= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= sigs.k8s.io/kustomize/api v0.17.2 h1:E7/Fjk7V5fboiuijoZHgs4aHuexi5Y2loXlVOAVAG5g= From 059b5d14d26c00eeee22565b83c21ba0f13a3150 Mon Sep 17 00:00:00 2001 From: Dexter Yan Date: Thu, 31 Oct 2024 03:19:30 +1300 Subject: [PATCH 24/25] fix(collector): limit run pod collector to delete only one related secret (#1668) * fix(collector): limit run pod collector to delete only related secret * change to ctx --- pkg/collect/run_pod.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/collect/run_pod.go b/pkg/collect/run_pod.go index 77da6d5f2..e430d6c99 100644 --- a/pkg/collect/run_pod.go +++ b/pkg/collect/run_pod.go @@ -63,9 +63,9 @@ func (c *CollectRunPod) Collect(progressChan chan<- interface{}) (result Collect if c.Collector.ImagePullSecret != nil && c.Collector.ImagePullSecret.Data != nil { defer func() { - for _, k := range pod.Spec.ImagePullSecrets { - if err := client.CoreV1().Secrets(pod.Namespace).Delete(context.Background(), k.Name, metav1.DeleteOptions{}); err != nil { - klog.Errorf("Failed to delete secret %s: %v", k.Name, err) + if c.Collector.ImagePullSecret.Name != "" { + if err := client.CoreV1().Secrets(pod.Namespace).Delete(ctx, c.Collector.ImagePullSecret.Name, metav1.DeleteOptions{}); err != nil { + klog.Errorf("Failed to delete secret %s: %v", c.Collector.ImagePullSecret.Name, err) } } }() From 544a700062c12b81e0d7e0c944faa81b93685003 Mon Sep 17 00:00:00 2001 From: Ash <159829404+hedge-sparrow@users.noreply.github.com> Date: Thu, 31 Oct 2024 10:44:35 +0000 Subject: [PATCH 25/25] [sc-114813] copy HostCollector fails to copy binary files when run in cluster (#1669) * Don't convert output bytes to string This prevents binary files getting mangled when the collector ourput is being passed around between functions * Update pkg/collect/runner.go Co-authored-by: Evans Mungai * organise imports --------- Co-authored-by: Evans Mungai --- pkg/collect/runner.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/pkg/collect/runner.go b/pkg/collect/runner.go index 40b0baa7f..c5ac7ae0d 100644 --- a/pkg/collect/runner.go +++ b/pkg/collect/runner.go @@ -1,11 +1,11 @@ package collect import ( + "bytes" "context" "encoding/json" "fmt" "strconv" - "strings" "time" "github.com/pkg/errors" @@ -57,7 +57,7 @@ func (r *podRunner) run(ctx context.Context, collector *troubleshootv1beta2.Host } results <- map[string][]byte{ - nodeName: []byte(logs), + nodeName: logs, } return nil @@ -314,16 +314,16 @@ func WaitForPodCompleted(ctx context.Context, client kubernetes.Interface, names }) } -func GetContainerLogs(ctx context.Context, client kubernetes.Interface, namespace string, podName string, containerName string, waitForComplete bool, interval time.Duration) (string, error) { +func GetContainerLogs(ctx context.Context, client kubernetes.Interface, namespace string, podName string, containerName string, waitForComplete bool, interval time.Duration) ([]byte, error) { if waitForComplete { if err := WaitForPodCompleted(ctx, client, namespace, podName, interval); err != nil { - return "", err + return nil, err } } return getContainerLogsInternal(ctx, client, namespace, podName, containerName, false) } -func getContainerLogsInternal(ctx context.Context, client kubernetes.Interface, namespace string, podName string, containerName string, previous bool) (string, error) { +func getContainerLogsInternal(ctx context.Context, client kubernetes.Interface, namespace string, podName string, containerName string, previous bool) ([]byte, error) { var logs []byte var err error @@ -349,11 +349,11 @@ func getContainerLogsInternal(ctx context.Context, client kubernetes.Interface, err = retry.OnError(retry.DefaultBackoff, retryableFn, logsFn) if err != nil { - return "", err + return nil, err } - if strings.Contains(string(logs), "Internal Error") { - return "", fmt.Errorf("Fetched log contains \"Internal Error\": %q", string(logs)) + if bytes.Contains(logs, []byte("Internal Error")) { + return nil, fmt.Errorf("Fetched log contains \"Internal Error\": %q", string(logs)) } - return string(logs), nil + return logs, nil }