From 56bacb5eaf4f2915c40dfc63ab650e42310b4821 Mon Sep 17 00:00:00 2001 From: Israel Blancas Date: Thu, 18 Jul 2024 20:26:23 +0200 Subject: [PATCH 01/14] Add must-gather tool to the operator image #3149 Signed-off-by: Israel Blancas --- .github/workflows/publish-images.yaml | 2 +- Dockerfile | 1 + Makefile | 5 +- cmd/gather/cluster/cluster.go | 317 ++++++++++++++++++++++++++ cmd/gather/cluster/write.go | 67 ++++++ cmd/gather/config/config.go | 41 ++++ cmd/gather/main.go | 49 ++++ 7 files changed, 480 insertions(+), 2 deletions(-) create mode 100644 cmd/gather/cluster/cluster.go create mode 100644 cmd/gather/cluster/write.go create mode 100644 cmd/gather/config/config.go create mode 100644 cmd/gather/main.go diff --git a/.github/workflows/publish-images.yaml b/.github/workflows/publish-images.yaml index 3c5fa49108..3c705c6e9e 100644 --- a/.github/workflows/publish-images.yaml +++ b/.github/workflows/publish-images.yaml @@ -47,7 +47,7 @@ jobs: for platform in $(echo $PLATFORMS | tr "," "\n"); do arch=${platform#*/} echo "Building manager for $arch" - make manager ARCH=$arch + make manager must-gather ARCH=$arch done - name: Docker meta diff --git a/Dockerfile b/Dockerfile index 3ab8a0336d..48104e6917 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,6 +15,7 @@ COPY --from=certificates /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-ce # Copy binary built on the host COPY bin/manager_${TARGETARCH} manager +COPY bin/must-gather_${TARGETARCH} must-gather USER 65532:65532 diff --git a/Makefile b/Makefile index 19e7d3ccd5..40d4b70016 100644 --- a/Makefile +++ b/Makefile @@ -140,6 +140,9 @@ ci: generate fmt vet test ensure-generate-is-noop manager: generate CGO_ENABLED=0 GOOS=$(GOOS) GOARCH=$(ARCH) go build -o bin/manager_${ARCH} -ldflags "${COMMON_LDFLAGS} ${OPERATOR_LDFLAGS}" main.go +must-gather: + CGO_ENABLED=0 GOOS=$(GOOS) GOARCH=$(ARCH) go build -o bin/must-gather_${ARCH} -ldflags "${COMMON_LDFLAGS} ${OPERATOR_LDFLAGS}" ./cmd/gather/main.go + # Build target allocator binary .PHONY: targetallocator targetallocator: @@ -328,7 +331,7 @@ scorecard-tests: operator-sdk # buildx is used to ensure same results for arm based systems (m1/2 chips) .PHONY: container container: GOOS = linux -container: manager +container: manager must-gather docker build -t ${IMG} . # Push the container image, used only for local dev purposes diff --git a/cmd/gather/cluster/cluster.go b/cmd/gather/cluster/cluster.go new file mode 100644 index 0000000000..226db2d9f4 --- /dev/null +++ b/cmd/gather/cluster/cluster.go @@ -0,0 +1,317 @@ +package cluster + +import ( + "context" + "fmt" + "log" + "os" + "path/filepath" + + otelv1alpha1 "github.com/open-telemetry/opentelemetry-operator/apis/v1alpha1" + otelv1beta1 "github.com/open-telemetry/opentelemetry-operator/apis/v1beta1" + "github.com/open-telemetry/opentelemetry-operator/cmd/gather/config" + routev1 "github.com/openshift/api/route/v1" + monitoringv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" + appsv1 "k8s.io/api/apps/v1" + autoscalingv2 "k8s.io/api/autoscaling/v2" + corev1 "k8s.io/api/core/v1" + networkingv1 "k8s.io/api/networking/v1" + policy1 "k8s.io/api/policy/v1" + rbacv1 "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/labels" + + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type Cluster struct { + config *config.Config +} + +func NewCluster(cfg *config.Config) Cluster { + return Cluster{config: cfg} +} + +func (c *Cluster) GetOpenTelemetryCollectors() error { + otelCols := otelv1beta1.OpenTelemetryCollectorList{} + + err := c.config.KubernetesClient.List(context.TODO(), &otelCols, &client.ListOptions{}) + if err != nil { + return err + } + + log.Println("OpenTelemetryCollectors found:", len(otelCols.Items)) + + errorDetected := false + + for _, otelCol := range otelCols.Items { + err := c.processOTELCollector(&otelCol) + if err != nil { + log.Fatalln(err) + errorDetected = true + } + } + + if errorDetected { + return fmt.Errorf("something failed while getting the opentelemtrycollectors") + } + return nil +} + +func (c *Cluster) GetInstrumentations() error { + instrumentations := otelv1alpha1.InstrumentationList{} + + err := c.config.KubernetesClient.List(context.TODO(), &instrumentations, &client.ListOptions{}) + if err != nil { + return err + } + + log.Println("Instrumentations found:", len(instrumentations.Items)) + + errorDetected := false + + for _, instr := range instrumentations.Items { + outputDir := filepath.Join(c.config.CollectionDir, instr.Namespace) + err := os.MkdirAll(outputDir, os.ModePerm) + if err != nil { + log.Fatalln(err) + errorDetected = true + continue + } + + writeToFile(outputDir, &instr) + + if err != nil { + + } + } + + if errorDetected { + return fmt.Errorf("something failed while getting the opentelemtrycollectors") + } + return nil +} + +func (c *Cluster) processOTELCollector(otelCol *otelv1beta1.OpenTelemetryCollector) error { + log.Printf("Processing OpenTelemetryCollector %s/%s", otelCol.Namespace, otelCol.Name) + folder, err := createFolder(c.config.CollectionDir, otelCol) + if err != nil { + return err + } + writeToFile(folder, otelCol) + + err = c.processOwnedResources(otelCol) + if err != nil { + return err + } + + return nil +} + +func (c *Cluster) processOwnedResources(otelCol *otelv1beta1.OpenTelemetryCollector) error { + folder, err := createFolder(c.config.CollectionDir, otelCol) + if err != nil { + return err + } + errorDetected := false + + // ClusterRole + crs := rbacv1.ClusterRoleList{} + err = c.getOwnerResources(&crs, otelCol) + if err != nil { + errorDetected = true + log.Fatalln(err) + } + for _, cr := range crs.Items { + writeToFile(folder, &cr) + } + + // ClusterRoleBindings + crbs := rbacv1.ClusterRoleBindingList{} + err = c.getOwnerResources(&crbs, otelCol) + if err != nil { + errorDetected = true + log.Fatalln(err) + } + for _, crb := range crbs.Items { + writeToFile(folder, &crb) + } + + // ConfigMaps + cms := corev1.ConfigMapList{} + err = c.getOwnerResources(&cms, otelCol) + if err != nil { + errorDetected = true + log.Fatalln(err) + } + for _, c := range cms.Items { + writeToFile(folder, &c) + } + + // DaemonSets + daemonsets := appsv1.DaemonSetList{} + err = c.getOwnerResources(&daemonsets, otelCol) + if err != nil { + errorDetected = true + log.Fatalln(err) + } + for _, d := range daemonsets.Items { + writeToFile(folder, &d) + } + + // Deployments + deployments := appsv1.DeploymentList{} + err = c.getOwnerResources(&deployments, otelCol) + if err != nil { + errorDetected = true + log.Fatalln(err) + } + for _, d := range deployments.Items { + writeToFile(folder, &d) + } + + // HPAs + hpas := autoscalingv2.HorizontalPodAutoscalerList{} + err = c.getOwnerResources(&hpas, otelCol) + if err != nil { + errorDetected = true + log.Fatalln(err) + } + for _, h := range hpas.Items { + writeToFile(folder, &h) + } + + // Ingresses + ingresses := networkingv1.IngressList{} + err = c.getOwnerResources(&ingresses, otelCol) + if err != nil { + errorDetected = true + log.Fatalln(err) + } + for _, i := range ingresses.Items { + writeToFile(folder, &i) + } + + // PersistentVolumes + pvs := corev1.PersistentVolumeList{} + err = c.getOwnerResources(&pvs, otelCol) + if err != nil { + errorDetected = true + log.Fatalln(err) + } + for _, p := range pvs.Items { + writeToFile(folder, &p) + } + + // PersistentVolumeClaims + pvcs := corev1.PersistentVolumeClaimList{} + err = c.getOwnerResources(&pvcs, otelCol) + if err != nil { + errorDetected = true + log.Fatalln(err) + } + for _, p := range pvcs.Items { + writeToFile(folder, &p) + } + + // PodDisruptionBudget + pdbs := policy1.PodDisruptionBudgetList{} + err = c.getOwnerResources(&pdbs, otelCol) + if err != nil { + errorDetected = true + log.Fatalln(err) + } + for _, pdb := range pdbs.Items { + writeToFile(folder, &pdb) + } + + // PodMonitors + pms := monitoringv1.PodMonitorList{} + err = c.getOwnerResources(&pms, otelCol) + if err != nil { + errorDetected = true + log.Fatalln(err) + } + for _, pm := range pms.Items { + writeToFile(folder, pm) + } + + // Routes + rs := routev1.RouteList{} + err = c.getOwnerResources(&rs, otelCol) + if err != nil { + errorDetected = true + log.Fatalln(err) + } + for _, r := range rs.Items { + writeToFile(folder, &r) + } + + // Services + services := corev1.ServiceList{} + err = c.getOwnerResources(&services, otelCol) + if err != nil { + errorDetected = true + log.Fatalln(err) + } + for _, s := range services.Items { + writeToFile(folder, &s) + } + + // ServiceMonitors + sms := monitoringv1.ServiceMonitorList{} + err = c.getOwnerResources(&sms, otelCol) + if err != nil { + errorDetected = true + log.Fatalln(err) + } + for _, s := range sms.Items { + writeToFile(folder, s) + } + + // ServiceAccounts + sas := corev1.ServiceAccountList{} + err = c.getOwnerResources(&sas, otelCol) + if err != nil { + errorDetected = true + log.Fatalln(err) + } + for _, s := range sas.Items { + writeToFile(folder, &s) + } + + // StatefulSets + statefulsets := appsv1.StatefulSetList{} + err = c.getOwnerResources(&statefulsets, otelCol) + if err != nil { + errorDetected = true + log.Fatalln(err) + } + for _, s := range statefulsets.Items { + writeToFile(folder, &s) + } + + if errorDetected { + return fmt.Errorf("something failed while getting the associated resources") + } + + return nil +} + +func (c *Cluster) getOwnerResources(objList client.ObjectList, otelCol *otelv1beta1.OpenTelemetryCollector) error { + return c.config.KubernetesClient.List(context.TODO(), objList, &client.ListOptions{ + LabelSelector: labels.SelectorFromSet(labels.Set{ + "app.kubernetes.io/instance": fmt.Sprintf("%s.%s", otelCol.Namespace, otelCol.Name), + "app.kubernetes.io/managed-by": "opentelemetry-operator", + "app.kubernetes.io/part-of": "opentelemetry", + }), + }) +} + +func hasOwnerReference(obj client.Object, otelCol *otelv1beta1.OpenTelemetryCollector) bool { + for _, ownerRef := range obj.GetOwnerReferences() { + if ownerRef.Kind == otelCol.Kind && ownerRef.UID == otelCol.UID { + return true + } + } + return false +} diff --git a/cmd/gather/cluster/write.go b/cmd/gather/cluster/write.go new file mode 100644 index 0000000000..f8d68beee2 --- /dev/null +++ b/cmd/gather/cluster/write.go @@ -0,0 +1,67 @@ +package cluster + +import ( + "fmt" + "log" + "os" + "path/filepath" + "reflect" + "strings" + + "github.com/open-telemetry/opentelemetry-operator/apis/v1beta1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/serializer/json" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func createFolder(collectionDir string, otelCol *v1beta1.OpenTelemetryCollector) (string, error) { + outputDir := filepath.Join(collectionDir, otelCol.Namespace, otelCol.Name) + err := os.MkdirAll(outputDir, os.ModePerm) + if err != nil { + return "", err + } + return outputDir, nil +} + +func createFile(outputDir string, obj client.Object) (*os.File, error) { + kind := obj.GetObjectKind().GroupVersionKind().Kind + + if kind == "" { + // reflect.TypeOf(obj) will return something like *v1.Deployment. We remove the first part + prefix, typeName, found := strings.Cut(reflect.TypeOf(obj).String(), ".") + if found { + kind = typeName + } else { + kind = prefix + } + } + + kind = strings.ToLower(kind) + + path := filepath.Join(outputDir, fmt.Sprintf("%s-%s.yaml", kind, obj.GetName())) + return os.Create(path) +} + +func writeToFile(outputDir string, o client.Object) { + // Open or create the file for writing + outputFile, err := createFile(outputDir, o) + if err != nil { + log.Fatalf("Failed to create file: %v", err) + } + defer outputFile.Close() + + unstructuredDeployment, err := runtime.DefaultUnstructuredConverter.ToUnstructured(o) + if err != nil { + log.Fatalf("Error converting deployment to unstructured: %v", err) + } + + unstructuredObj := &unstructured.Unstructured{Object: unstructuredDeployment} + + // Serialize the unstructured object to YAML + serializer := json.NewYAMLSerializer(json.DefaultMetaFactory, nil, nil) + err = serializer.Encode(unstructuredObj, outputFile) + if err != nil { + log.Fatalf("Error encoding to YAML: %v", err) + } +} diff --git a/cmd/gather/config/config.go b/cmd/gather/config/config.go new file mode 100644 index 0000000000..6008b3f260 --- /dev/null +++ b/cmd/gather/config/config.go @@ -0,0 +1,41 @@ +package config + +import ( + "fmt" + "path/filepath" + + "github.com/spf13/pflag" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/tools/clientcmd" + "k8s.io/client-go/util/homedir" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type Config struct { + CollectionDir string + KubernetesClient client.Client +} + +func NewConfig(scheme *runtime.Scheme) (Config, error) { + var kubeconfigPath string + var collectionDir string + + pflag.StringVar(&kubeconfigPath, "kubeconfig-path", filepath.Join(homedir.HomeDir(), ".kube", "config"), "Absolute path to the KubeconfigPath file") + pflag.StringVar(&collectionDir, "collection-dir", filepath.Join(homedir.HomeDir(), "must-gather"), "Absolute path to the KubeconfigPath file") + pflag.Parse() + + config, err := clientcmd.BuildConfigFromFlags("", kubeconfigPath) + if err != nil { + return Config{}, fmt.Errorf("Error reading the kubeconfig: %s\n", err.Error()) + } + + clusterClient, err := client.New(config, client.Options{Scheme: scheme}) + if err != nil { + return Config{}, fmt.Errorf("Creating the Kubernetes client: %s\n", err) + } + + return Config{ + CollectionDir: collectionDir, + KubernetesClient: clusterClient, + }, nil +} diff --git a/cmd/gather/main.go b/cmd/gather/main.go new file mode 100644 index 0000000000..d543669b1b --- /dev/null +++ b/cmd/gather/main.go @@ -0,0 +1,49 @@ +package main + +import ( + "fmt" + "os" + + otelv1alpha1 "github.com/open-telemetry/opentelemetry-operator/apis/v1alpha1" + otelv1beta1 "github.com/open-telemetry/opentelemetry-operator/apis/v1beta1" + "github.com/open-telemetry/opentelemetry-operator/cmd/gather/cluster" + "github.com/open-telemetry/opentelemetry-operator/cmd/gather/config" + routev1 "github.com/openshift/api/route/v1" + monitoringv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" + appsv1 "k8s.io/api/apps/v1" + autoscalingv2 "k8s.io/api/autoscaling/v2" + corev1 "k8s.io/api/core/v1" + networkingv1 "k8s.io/api/networking/v1" + policyV1 "k8s.io/api/policy/v1" + rbacv1 "k8s.io/api/rbac/v1" + k8sruntime "k8s.io/apimachinery/pkg/runtime" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" +) + +var scheme *k8sruntime.Scheme + +func init() { + scheme = k8sruntime.NewScheme() + utilruntime.Must(otelv1alpha1.AddToScheme(scheme)) + utilruntime.Must(otelv1beta1.AddToScheme(scheme)) + utilruntime.Must(appsv1.AddToScheme(scheme)) + utilruntime.Must(corev1.AddToScheme(scheme)) + utilruntime.Must(networkingv1.AddToScheme(scheme)) + utilruntime.Must(autoscalingv2.AddToScheme(scheme)) + utilruntime.Must(rbacv1.AddToScheme(scheme)) + utilruntime.Must(policyV1.AddToScheme(scheme)) + utilruntime.Must(monitoringv1.AddToScheme(scheme)) + utilruntime.Must(routev1.AddToScheme(scheme)) +} + +func main() { + config, err := config.NewConfig(scheme) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + cluster := cluster.NewCluster(&config) + cluster.GetOpenTelemetryCollectors() + cluster.GetInstrumentations() +} From 82bda69e0115e9ca13a85ceec60b64bff43d6c73 Mon Sep 17 00:00:00 2001 From: Israel Blancas Date: Fri, 19 Jul 2024 18:07:59 +0200 Subject: [PATCH 02/14] Add some missing features Signed-off-by: Israel Blancas --- cmd/gather/cluster/cluster.go | 385 ++++++++++++++++++++++++---------- cmd/gather/cluster/write.go | 7 +- cmd/gather/config/config.go | 17 +- cmd/gather/main.go | 6 + go.mod | 4 +- go.sum | 9 +- 6 files changed, 310 insertions(+), 118 deletions(-) diff --git a/cmd/gather/cluster/cluster.go b/cmd/gather/cluster/cluster.go index 226db2d9f4..db5d86a924 100644 --- a/cmd/gather/cluster/cluster.go +++ b/cmd/gather/cluster/cluster.go @@ -6,11 +6,15 @@ import ( "log" "os" "path/filepath" + "reflect" + "strings" otelv1alpha1 "github.com/open-telemetry/opentelemetry-operator/apis/v1alpha1" otelv1beta1 "github.com/open-telemetry/opentelemetry-operator/apis/v1beta1" "github.com/open-telemetry/opentelemetry-operator/cmd/gather/config" routev1 "github.com/openshift/api/route/v1" + operatorsv1 "github.com/operator-framework/api/pkg/operators/v1" + operatorsv1alpha1 "github.com/operator-framework/api/pkg/operators/v1alpha1" monitoringv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" appsv1 "k8s.io/api/apps/v1" autoscalingv2 "k8s.io/api/autoscaling/v2" @@ -19,7 +23,7 @@ import ( policy1 "k8s.io/api/policy/v1" rbacv1 "k8s.io/api/rbac/v1" "k8s.io/apimachinery/pkg/labels" - + "k8s.io/apimachinery/pkg/runtime/schema" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -31,10 +35,151 @@ func NewCluster(cfg *config.Config) Cluster { return Cluster{config: cfg} } +func (c *Cluster) getOperatorNamespace() (string, error) { + if c.config.OperatorNamespace != "" { + return c.config.OperatorNamespace, nil + } + + deployment, err := c.getOperatorDeployment() + if err != nil { + return "", err + } + + c.config.OperatorNamespace = deployment.Namespace + + return c.config.OperatorNamespace, nil +} + +func (c *Cluster) getOperatorDeployment() (appsv1.Deployment, error) { + operatorDeployments := appsv1.DeploymentList{} + err := c.config.KubernetesClient.List(context.TODO(), &operatorDeployments, &client.ListOptions{ + LabelSelector: labels.SelectorFromSet(labels.Set{ + "app.kubernetes.io/name": "opentelemetry-operator", + }), + }) + + if err != nil { + return appsv1.Deployment{}, err + } + + if len(operatorDeployments.Items) == 0 { + return appsv1.Deployment{}, fmt.Errorf("operator not found") + } + + return operatorDeployments.Items[0], nil + +} + +func (c *Cluster) GetOperatorDeploymentInfo() error { + err := os.MkdirAll(c.config.CollectionDir, os.ModePerm) + if err != nil { + return err + } + + deployment, err := c.getOperatorDeployment() + if err != nil { + return err + } + + writeToFile(c.config.CollectionDir, &deployment) + + return nil +} + +func (c *Cluster) GetOLMInfo() error { + if !c.isAPIAvailable(schema.GroupVersionResource{ + Group: operatorsv1.SchemeGroupVersion.Group, + Version: operatorsv1.SchemeGroupVersion.Version, + Resource: "Operator", + }) { + log.Println("OLM info not available") + return nil + } + + outputDir := filepath.Join(c.config.CollectionDir, "olm") + err := os.MkdirAll(outputDir, os.ModePerm) + if err != nil { + return err + } + + operatorNamespace, err := c.getOperatorNamespace() + if err != nil { + return err + } + + // Operators + operators := operatorsv1.OperatorList{} + err = c.config.KubernetesClient.List(context.TODO(), &operators, &client.ListOptions{ + Namespace: operatorNamespace, + }) + if err != nil { + return err + } + for _, o := range operators.Items { + writeToFile(outputDir, &o) + + } + + // OperatorGroups + operatorGroups := operatorsv1.OperatorGroupList{} + err = c.config.KubernetesClient.List(context.TODO(), &operatorGroups, &client.ListOptions{ + Namespace: operatorNamespace, + }) + if err != nil { + return err + } + for _, o := range operatorGroups.Items { + if strings.Contains(o.Name, "opentelemetry") { + writeToFile(outputDir, &o) + } + } + + // Subscription + subscriptions := operatorsv1alpha1.SubscriptionList{} + err = c.config.KubernetesClient.List(context.TODO(), &subscriptions, &client.ListOptions{ + Namespace: operatorNamespace, + }) + if err != nil { + return err + } + for _, o := range subscriptions.Items { + writeToFile(outputDir, &o) + + } + + // InstallPlan + ips := operatorsv1alpha1.InstallPlanList{} + err = c.config.KubernetesClient.List(context.TODO(), &ips, &client.ListOptions{ + Namespace: operatorNamespace, + }) + if err != nil { + return err + } + for _, o := range ips.Items { + writeToFile(outputDir, &o) + } + + // ClusterServiceVersion + csvs := operatorsv1alpha1.ClusterServiceVersionList{} + err = c.config.KubernetesClient.List(context.TODO(), &csvs, &client.ListOptions{ + Namespace: operatorNamespace, + }) + if err != nil { + return err + } + for _, o := range csvs.Items { + if strings.Contains(o.Name, "opentelemetry") { + writeToFile(outputDir, &o) + } + } + + return nil +} + func (c *Cluster) GetOpenTelemetryCollectors() error { otelCols := otelv1beta1.OpenTelemetryCollectorList{} - err := c.config.KubernetesClient.List(context.TODO(), &otelCols, &client.ListOptions{}) + err := c.config.KubernetesClient.List(context.TODO(), &otelCols) if err != nil { return err } @@ -60,7 +205,7 @@ func (c *Cluster) GetOpenTelemetryCollectors() error { func (c *Cluster) GetInstrumentations() error { instrumentations := otelv1alpha1.InstrumentationList{} - err := c.config.KubernetesClient.List(context.TODO(), &instrumentations, &client.ListOptions{}) + err := c.config.KubernetesClient.List(context.TODO(), &instrumentations) if err != nil { return err } @@ -79,10 +224,6 @@ func (c *Cluster) GetInstrumentations() error { } writeToFile(outputDir, &instr) - - if err != nil { - - } } if errorDetected { @@ -93,13 +234,13 @@ func (c *Cluster) GetInstrumentations() error { func (c *Cluster) processOTELCollector(otelCol *otelv1beta1.OpenTelemetryCollector) error { log.Printf("Processing OpenTelemetryCollector %s/%s", otelCol.Namespace, otelCol.Name) - folder, err := createFolder(c.config.CollectionDir, otelCol) + folder, err := createOTELFolder(c.config.CollectionDir, otelCol) if err != nil { return err } writeToFile(folder, otelCol) - err = c.processOwnedResources(otelCol) + err = c.processOwnedResources(otelCol, folder) if err != nil { return err } @@ -107,187 +248,197 @@ func (c *Cluster) processOTELCollector(otelCol *otelv1beta1.OpenTelemetryCollect return nil } -func (c *Cluster) processOwnedResources(otelCol *otelv1beta1.OpenTelemetryCollector) error { - folder, err := createFolder(c.config.CollectionDir, otelCol) - if err != nil { - return err - } +func (c *Cluster) processOwnedResources(otelCol *otelv1beta1.OpenTelemetryCollector, folder string) error { errorDetected := false - // ClusterRole - crs := rbacv1.ClusterRoleList{} - err = c.getOwnerResources(&crs, otelCol) - if err != nil { - errorDetected = true - log.Fatalln(err) - } - for _, cr := range crs.Items { - writeToFile(folder, &cr) - } - - // ClusterRoleBindings - crbs := rbacv1.ClusterRoleBindingList{} - err = c.getOwnerResources(&crbs, otelCol) + ////////////////////////////////////////////////////////////////// apps/v1 + // DaemonSets + daemonsets, err := c.getOwnerResources(&appsv1.DaemonSetList{}, otelCol) if err != nil { errorDetected = true log.Fatalln(err) } - for _, crb := range crbs.Items { - writeToFile(folder, &crb) + for _, d := range daemonsets { + writeToFile(folder, d) } - // ConfigMaps - cms := corev1.ConfigMapList{} - err = c.getOwnerResources(&cms, otelCol) + // Deployments + deployments, err := c.getOwnerResources(&appsv1.DeploymentList{}, otelCol) if err != nil { errorDetected = true log.Fatalln(err) } - for _, c := range cms.Items { - writeToFile(folder, &c) + for _, d := range deployments { + writeToFile(folder, d) } - // DaemonSets - daemonsets := appsv1.DaemonSetList{} - err = c.getOwnerResources(&daemonsets, otelCol) + // StatefulSets + statefulsets, err := c.getOwnerResources(&appsv1.StatefulSetList{}, otelCol) if err != nil { errorDetected = true log.Fatalln(err) } - for _, d := range daemonsets.Items { - writeToFile(folder, &d) + for _, s := range statefulsets { + writeToFile(folder, s) } - // Deployments - deployments := appsv1.DeploymentList{} - err = c.getOwnerResources(&deployments, otelCol) + ////////////////////////////////////////////////////////////////// rbac/v1 + // ClusterRole + crs, err := c.getOwnerResources(&rbacv1.ClusterRoleList{}, otelCol) if err != nil { errorDetected = true log.Fatalln(err) } - for _, d := range deployments.Items { - writeToFile(folder, &d) + for _, cr := range crs { + writeToFile(folder, cr) } - // HPAs - hpas := autoscalingv2.HorizontalPodAutoscalerList{} - err = c.getOwnerResources(&hpas, otelCol) + // ClusterRoleBindings + crbs, err := c.getOwnerResources(&rbacv1.ClusterRoleBindingList{}, otelCol) if err != nil { errorDetected = true log.Fatalln(err) } - for _, h := range hpas.Items { - writeToFile(folder, &h) + for _, crb := range crbs { + writeToFile(folder, crb) } - // Ingresses - ingresses := networkingv1.IngressList{} - err = c.getOwnerResources(&ingresses, otelCol) + ////////////////////////////////////////////////////////////////// core/v1 + // ConfigMaps + cms, err := c.getOwnerResources(&corev1.ConfigMapList{}, otelCol) if err != nil { errorDetected = true log.Fatalln(err) } - for _, i := range ingresses.Items { - writeToFile(folder, &i) + for _, c := range cms { + writeToFile(folder, c) } // PersistentVolumes - pvs := corev1.PersistentVolumeList{} - err = c.getOwnerResources(&pvs, otelCol) + pvs, err := c.getOwnerResources(&corev1.PersistentVolumeList{}, otelCol) if err != nil { errorDetected = true log.Fatalln(err) } - for _, p := range pvs.Items { - writeToFile(folder, &p) + for _, p := range pvs { + writeToFile(folder, p) } // PersistentVolumeClaims - pvcs := corev1.PersistentVolumeClaimList{} - err = c.getOwnerResources(&pvcs, otelCol) + pvcs, err := c.getOwnerResources(&corev1.PersistentVolumeClaimList{}, otelCol) if err != nil { errorDetected = true log.Fatalln(err) } - for _, p := range pvcs.Items { - writeToFile(folder, &p) + for _, p := range pvcs { + writeToFile(folder, p) } - // PodDisruptionBudget - pdbs := policy1.PodDisruptionBudgetList{} - err = c.getOwnerResources(&pdbs, otelCol) + // Pods + pods, err := c.getOwnerResources(&corev1.PodList{}, otelCol) if err != nil { errorDetected = true log.Fatalln(err) } - for _, pdb := range pdbs.Items { - writeToFile(folder, &pdb) + for _, p := range pods { + writeToFile(folder, p) } - // PodMonitors - pms := monitoringv1.PodMonitorList{} - err = c.getOwnerResources(&pms, otelCol) + // Services + services, err := c.getOwnerResources(&corev1.ServiceList{}, otelCol) if err != nil { errorDetected = true log.Fatalln(err) } - for _, pm := range pms.Items { - writeToFile(folder, pm) + for _, s := range services { + writeToFile(folder, s) } - // Routes - rs := routev1.RouteList{} - err = c.getOwnerResources(&rs, otelCol) + // ServiceAccounts + sas, err := c.getOwnerResources(&corev1.ServiceAccountList{}, otelCol) if err != nil { errorDetected = true log.Fatalln(err) } - for _, r := range rs.Items { - writeToFile(folder, &r) + for _, s := range sas { + writeToFile(folder, s) } - // Services - services := corev1.ServiceList{} - err = c.getOwnerResources(&services, otelCol) + ////////////////////////////////////////////////////////////////// autoscaling/v2 + // HPAs + hpas, err := c.getOwnerResources(&autoscalingv2.HorizontalPodAutoscalerList{}, otelCol) if err != nil { errorDetected = true log.Fatalln(err) } - for _, s := range services.Items { - writeToFile(folder, &s) + for _, h := range hpas { + writeToFile(folder, h) } - // ServiceMonitors - sms := monitoringv1.ServiceMonitorList{} - err = c.getOwnerResources(&sms, otelCol) + ////////////////////////////////////////////////////////////////// networking/v1 + // Ingresses + ingresses, err := c.getOwnerResources(&networkingv1.IngressList{}, otelCol) if err != nil { errorDetected = true log.Fatalln(err) } - for _, s := range sms.Items { - writeToFile(folder, s) + for _, i := range ingresses { + writeToFile(folder, i) } - // ServiceAccounts - sas := corev1.ServiceAccountList{} - err = c.getOwnerResources(&sas, otelCol) + ////////////////////////////////////////////////////////////////// policy/v1 + // PodDisruptionBudge + pdbs, err := c.getOwnerResources(&policy1.PodDisruptionBudgetList{}, otelCol) if err != nil { errorDetected = true log.Fatalln(err) } - for _, s := range sas.Items { - writeToFile(folder, &s) + for _, pdb := range pdbs { + writeToFile(folder, pdb) } - // StatefulSets - statefulsets := appsv1.StatefulSetList{} - err = c.getOwnerResources(&statefulsets, otelCol) - if err != nil { - errorDetected = true - log.Fatalln(err) + ////////////////////////////////////////////////////////////////// monitoring/v1 + if c.isAPIAvailable(schema.GroupVersionResource{ + Group: monitoringv1.SchemeGroupVersion.Group, + Version: monitoringv1.SchemeGroupVersion.Version, + Resource: "ServiceMonitor", + }) { + // PodMonitors + pms, err := c.getOwnerResources(&monitoringv1.PodMonitorList{}, otelCol) + if err != nil { + errorDetected = true + log.Fatalln(err) + } + for _, pm := range pms { + writeToFile(folder, pm) + } + + // ServiceMonitors + sms, err := c.getOwnerResources(&monitoringv1.ServiceMonitorList{}, otelCol) + if err != nil { + errorDetected = true + log.Fatalln(err) + } + for _, s := range sms { + writeToFile(folder, s) + } } - for _, s := range statefulsets.Items { - writeToFile(folder, &s) + + ////////////////////////////////////////////////////////////////// route/v1 + // Routes + if c.isAPIAvailable(schema.GroupVersionResource{ + Group: routev1.GroupName, + Version: routev1.GroupVersion.Version, + Resource: "Route", + }) { + rs, err := c.getOwnerResources(&routev1.RouteList{}, otelCol) + if err != nil { + errorDetected = true + log.Fatalln(err) + } + for _, r := range rs { + writeToFile(folder, r) + } } if errorDetected { @@ -297,14 +448,38 @@ func (c *Cluster) processOwnedResources(otelCol *otelv1beta1.OpenTelemetryCollec return nil } -func (c *Cluster) getOwnerResources(objList client.ObjectList, otelCol *otelv1beta1.OpenTelemetryCollector) error { - return c.config.KubernetesClient.List(context.TODO(), objList, &client.ListOptions{ +func (c *Cluster) getOwnerResources(objList client.ObjectList, otelCol *otelv1beta1.OpenTelemetryCollector) ([]client.Object, error) { + err := c.config.KubernetesClient.List(context.TODO(), objList, &client.ListOptions{ LabelSelector: labels.SelectorFromSet(labels.Set{ - "app.kubernetes.io/instance": fmt.Sprintf("%s.%s", otelCol.Namespace, otelCol.Name), "app.kubernetes.io/managed-by": "opentelemetry-operator", - "app.kubernetes.io/part-of": "opentelemetry", }), }) + if err != nil { + return nil, err + } + + resources := []client.Object{} + + items := reflect.ValueOf(objList).Elem().FieldByName("Items") + for i := 0; i < items.Len(); i++ { + item := items.Index(i).Addr().Interface().(client.Object) + if hasOwnerReference(item, otelCol) { + resources = append(resources, item) + } + } + return resources, nil + +} + +func (c *Cluster) isAPIAvailable(gvr schema.GroupVersionResource) bool { + rm := c.config.KubernetesClient.RESTMapper() + + gvk, err := rm.KindFor(gvr) + if err != nil { + return false + } + + return !gvk.Empty() } func hasOwnerReference(obj client.Object, otelCol *otelv1beta1.OpenTelemetryCollector) bool { diff --git a/cmd/gather/cluster/write.go b/cmd/gather/cluster/write.go index f8d68beee2..45b519740c 100644 --- a/cmd/gather/cluster/write.go +++ b/cmd/gather/cluster/write.go @@ -15,8 +15,8 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" ) -func createFolder(collectionDir string, otelCol *v1beta1.OpenTelemetryCollector) (string, error) { - outputDir := filepath.Join(collectionDir, otelCol.Namespace, otelCol.Name) +func createOTELFolder(collectionDir string, otelCol *v1beta1.OpenTelemetryCollector) (string, error) { + outputDir := filepath.Join(collectionDir, "namespaces", otelCol.Namespace, otelCol.Name) err := os.MkdirAll(outputDir, os.ModePerm) if err != nil { return "", err @@ -38,8 +38,9 @@ func createFile(outputDir string, obj client.Object) (*os.File, error) { } kind = strings.ToLower(kind) + name := strings.ReplaceAll(obj.GetName(), ".", "-") - path := filepath.Join(outputDir, fmt.Sprintf("%s-%s.yaml", kind, obj.GetName())) + path := filepath.Join(outputDir, fmt.Sprintf("%s-%s.yaml", kind, name)) return os.Create(path) } diff --git a/cmd/gather/config/config.go b/cmd/gather/config/config.go index 6008b3f260..467726be56 100644 --- a/cmd/gather/config/config.go +++ b/cmd/gather/config/config.go @@ -12,14 +12,17 @@ import ( ) type Config struct { - CollectionDir string - KubernetesClient client.Client + CollectionDir string + OperatorName string + OperatorNamespace string + KubernetesClient client.Client } func NewConfig(scheme *runtime.Scheme) (Config, error) { - var kubeconfigPath string - var collectionDir string + var operatorName, operatorNamespace, kubeconfigPath, collectionDir string + pflag.StringVar(&operatorName, "operator-name", "opentelemetry-operator", "Operator name") + pflag.StringVar(&operatorNamespace, "operator-namespace", "", "Namespace where the operator was deployed") pflag.StringVar(&kubeconfigPath, "kubeconfig-path", filepath.Join(homedir.HomeDir(), ".kube", "config"), "Absolute path to the KubeconfigPath file") pflag.StringVar(&collectionDir, "collection-dir", filepath.Join(homedir.HomeDir(), "must-gather"), "Absolute path to the KubeconfigPath file") pflag.Parse() @@ -35,7 +38,9 @@ func NewConfig(scheme *runtime.Scheme) (Config, error) { } return Config{ - CollectionDir: collectionDir, - KubernetesClient: clusterClient, + CollectionDir: collectionDir, + KubernetesClient: clusterClient, + OperatorName: operatorName, + OperatorNamespace: operatorNamespace, }, nil } diff --git a/cmd/gather/main.go b/cmd/gather/main.go index d543669b1b..94bd1a058a 100644 --- a/cmd/gather/main.go +++ b/cmd/gather/main.go @@ -9,6 +9,8 @@ import ( "github.com/open-telemetry/opentelemetry-operator/cmd/gather/cluster" "github.com/open-telemetry/opentelemetry-operator/cmd/gather/config" routev1 "github.com/openshift/api/route/v1" + operatorsv1 "github.com/operator-framework/api/pkg/operators/v1" + operatorsv1alpha1 "github.com/operator-framework/api/pkg/operators/v1alpha1" monitoringv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" appsv1 "k8s.io/api/apps/v1" autoscalingv2 "k8s.io/api/autoscaling/v2" @@ -34,6 +36,8 @@ func init() { utilruntime.Must(policyV1.AddToScheme(scheme)) utilruntime.Must(monitoringv1.AddToScheme(scheme)) utilruntime.Must(routev1.AddToScheme(scheme)) + utilruntime.Must(operatorsv1.AddToScheme(scheme)) + utilruntime.Must(operatorsv1alpha1.AddToScheme(scheme)) } func main() { @@ -44,6 +48,8 @@ func main() { } cluster := cluster.NewCluster(&config) + cluster.GetOperatorDeploymentInfo() + cluster.GetOLMInfo() cluster.GetOpenTelemetryCollectors() cluster.GetInstrumentations() } diff --git a/go.mod b/go.mod index de7ab6353f..70a4aae4cf 100644 --- a/go.mod +++ b/go.mod @@ -19,6 +19,7 @@ require ( github.com/oklog/ulid/v2 v2.1.0 github.com/open-telemetry/opamp-go v0.14.0 github.com/openshift/api v0.0.0-20240124164020-e2ce40831f2e + github.com/operator-framework/api v0.24.0 github.com/operator-framework/operator-lib v0.14.0 github.com/prometheus-operator/prometheus-operator v0.75.0 github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.75.1 @@ -183,7 +184,8 @@ require ( github.com/prometheus/common/sigv4 v0.1.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect github.com/scaleway/scaleway-sdk-go v1.0.0-beta.27 // indirect - github.com/spf13/cobra v1.7.0 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/spf13/cobra v1.8.0 // indirect github.com/tklauser/go-sysconf v0.3.13 // indirect github.com/tklauser/numcpus v0.7.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect diff --git a/go.sum b/go.sum index 643c2fcc1a..7b9115595c 100644 --- a/go.sum +++ b/go.sum @@ -121,7 +121,7 @@ github.com/cncf/xds/go v0.0.0-20240318125728-8a4994d93e50 h1:DBmgJDC9dTfkVyGgipa github.com/cncf/xds/go v0.0.0-20240318125728-8a4994d93e50/go.mod h1:5e1+Vvlzido69INQaVO6d87Qn543Xr6nooe9Kz7oBFM= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= -github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -509,6 +509,8 @@ github.com/openshift/api v0.0.0-20240124164020-e2ce40831f2e h1:cxgCNo/R769CO23AK github.com/openshift/api v0.0.0-20240124164020-e2ce40831f2e/go.mod h1:CxgbWAlvu2iQB0UmKTtRu1YfepRg1/vJ64n2DlIEVz4= github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= +github.com/operator-framework/api v0.24.0 h1:fHynWEzuY/YhUTlsK9hd+QQ0bZcFakxCTdaZbFaVXbc= +github.com/operator-framework/api v0.24.0/go.mod h1:EXKrka63NyQDDpWZ+DGTDEliNV0xRq6UMZRoUPhilVM= github.com/operator-framework/operator-lib v0.14.0 h1:er+BgZymZD1im2wytLJiPLZpGALAX6N0gXaHx3PKbO4= github.com/operator-framework/operator-lib v0.14.0/go.mod h1:wUu4Xb9xzXnIpglvaZ3yucTMSlqGXHIoUEH9+5gWiu0= github.com/ovh/go-ovh v1.5.1 h1:P8O+7H+NQuFK9P/j4sFW5C0fvSS2DnHYGPwdVCp45wI= @@ -590,8 +592,8 @@ github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6Mwd github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= -github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= +github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= +github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -838,6 +840,7 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= From 9e4e9d4c2cc29b3b09cdfd39e05c4581c93e2bc8 Mon Sep 17 00:00:00 2001 From: Israel Blancas Date: Fri, 26 Jul 2024 12:31:57 +0200 Subject: [PATCH 03/14] Add operator logs Signed-off-by: Israel Blancas --- cmd/gather/cluster/cluster.go | 25 +++++++++++++++++++++++++ cmd/gather/cluster/write.go | 31 +++++++++++++++++++++++++++++++ cmd/gather/config/config.go | 24 ++++++++++++++++-------- cmd/gather/main.go | 1 + 4 files changed, 73 insertions(+), 8 deletions(-) diff --git a/cmd/gather/cluster/cluster.go b/cmd/gather/cluster/cluster.go index db5d86a924..fdde0b5e73 100644 --- a/cmd/gather/cluster/cluster.go +++ b/cmd/gather/cluster/cluster.go @@ -70,6 +70,31 @@ func (c *Cluster) getOperatorDeployment() (appsv1.Deployment, error) { } +func (c *Cluster) GetOperatorLogs() error { + deployment, err := c.getOperatorDeployment() + if err != nil { + return err + } + + labelSelector := labels.Set(deployment.Spec.Selector.MatchLabels).AsSelectorPreValidated() + operatorPods := corev1.PodList{} + err = c.config.KubernetesClient.List(context.TODO(), &operatorPods, &client.ListOptions{ + LabelSelector: labelSelector, + }) + if err != nil { + return err + } + + pod := operatorPods.Items[0] + c.getPodLogs(pod.Name, pod.Namespace, "manager") + return nil +} + +func (c *Cluster) getPodLogs(podName, namespace, container string) { + pods := c.config.KubernetesClientSet.CoreV1().Pods(namespace) + writeLogToFile(c.config.CollectionDir, podName, container, pods) +} + func (c *Cluster) GetOperatorDeploymentInfo() error { err := os.MkdirAll(c.config.CollectionDir, os.ModePerm) if err != nil { diff --git a/cmd/gather/cluster/write.go b/cmd/gather/cluster/write.go index 45b519740c..762143fbb4 100644 --- a/cmd/gather/cluster/write.go +++ b/cmd/gather/cluster/write.go @@ -1,7 +1,9 @@ package cluster import ( + "context" "fmt" + "io" "log" "os" "path/filepath" @@ -9,9 +11,11 @@ import ( "strings" "github.com/open-telemetry/opentelemetry-operator/apis/v1beta1" + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/serializer/json" + cgocorev1 "k8s.io/client-go/kubernetes/typed/core/v1" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -44,6 +48,33 @@ func createFile(outputDir string, obj client.Object) (*os.File, error) { return os.Create(path) } +func writeLogToFile(outputDir, podName, container string, p cgocorev1.PodInterface) { + req := p.GetLogs(podName, &corev1.PodLogOptions{Container: container}) + podLogs, err := req.Stream(context.Background()) + if err != nil { + log.Fatalf("Error getting pod logs: %v\n", err) + return + } + defer podLogs.Close() + + err = os.MkdirAll(outputDir, os.ModePerm) + if err != nil { + log.Fatalln(err) + return + } + + outputFile, err := os.Create(filepath.Join(outputDir, podName)) + if err != nil { + log.Fatalf("Error getting pod logs: %v\n", err) + return + } + + _, err = io.Copy(outputFile, podLogs) + if err != nil { + log.Fatalf("Error copying logs to file: %v\n", err) + } +} + func writeToFile(outputDir string, o client.Object) { // Open or create the file for writing outputFile, err := createFile(outputDir, o) diff --git a/cmd/gather/config/config.go b/cmd/gather/config/config.go index 467726be56..0866c15b6f 100644 --- a/cmd/gather/config/config.go +++ b/cmd/gather/config/config.go @@ -6,16 +6,18 @@ import ( "github.com/spf13/pflag" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/kubernetes" "k8s.io/client-go/tools/clientcmd" "k8s.io/client-go/util/homedir" "sigs.k8s.io/controller-runtime/pkg/client" ) type Config struct { - CollectionDir string - OperatorName string - OperatorNamespace string - KubernetesClient client.Client + CollectionDir string + OperatorName string + OperatorNamespace string + KubernetesClient client.Client + KubernetesClientSet *kubernetes.Clientset } func NewConfig(scheme *runtime.Scheme) (Config, error) { @@ -37,10 +39,16 @@ func NewConfig(scheme *runtime.Scheme) (Config, error) { return Config{}, fmt.Errorf("Creating the Kubernetes client: %s\n", err) } + clientset, err := kubernetes.NewForConfig(config) + if err != nil { + return Config{}, fmt.Errorf("Creating the Kubernetes clienset: %s\n", err) + } + return Config{ - CollectionDir: collectionDir, - KubernetesClient: clusterClient, - OperatorName: operatorName, - OperatorNamespace: operatorNamespace, + CollectionDir: collectionDir, + KubernetesClient: clusterClient, + KubernetesClientSet: clientset, + OperatorName: operatorName, + OperatorNamespace: operatorNamespace, }, nil } diff --git a/cmd/gather/main.go b/cmd/gather/main.go index 94bd1a058a..d4d0e24d3d 100644 --- a/cmd/gather/main.go +++ b/cmd/gather/main.go @@ -48,6 +48,7 @@ func main() { } cluster := cluster.NewCluster(&config) + cluster.GetOperatorLogs() cluster.GetOperatorDeploymentInfo() cluster.GetOLMInfo() cluster.GetOpenTelemetryCollectors() From ec882610d250e520d1daf6aa8c24c092b426433b Mon Sep 17 00:00:00 2001 From: Israel Blancas Date: Fri, 26 Jul 2024 12:35:39 +0200 Subject: [PATCH 04/14] Add changelog Signed-off-by: Israel Blancas --- .chloggen/3149-add-must-gather.yaml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100755 .chloggen/3149-add-must-gather.yaml diff --git a/.chloggen/3149-add-must-gather.yaml b/.chloggen/3149-add-must-gather.yaml new file mode 100755 index 0000000000..b5a1deb601 --- /dev/null +++ b/.chloggen/3149-add-must-gather.yaml @@ -0,0 +1,16 @@ +# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix' +change_type: enhancement + +# The name of the component, or a single word describing the area of concern, (e.g. collector, target allocator, auto-instrumentation, opamp, github action) +component: operator + +# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). +note: "Add a must gather application to help troubleshoot" + +# One or more tracking issues related to the change +issues: [3149] + +# (Optional) One or more lines of additional information to render under the primary note. +# These lines will be padded with 2 spaces and then inserted directly into the document. +# Use pipe (|) for multiline entries. +subtext: "The new application was added to the operator container so it can be run without adding any new container. It can be run from the /must-gather path" \ No newline at end of file From 765fe875df57be4dbb5a62ce05f2b88daec34db6 Mon Sep 17 00:00:00 2001 From: Israel Blancas Date: Mon, 5 Aug 2024 13:02:00 +0200 Subject: [PATCH 05/14] Fix lint Signed-off-by: Israel Blancas --- cmd/gather/cluster/cluster.go | 28 +++++++++++++++++-- cmd/gather/cluster/write.go | 17 +++++++++++- cmd/gather/config/config.go | 20 ++++++++++++-- cmd/gather/main.go | 52 +++++++++++++++++++++++++++-------- 4 files changed, 99 insertions(+), 18 deletions(-) diff --git a/cmd/gather/cluster/cluster.go b/cmd/gather/cluster/cluster.go index fdde0b5e73..e6299833d8 100644 --- a/cmd/gather/cluster/cluster.go +++ b/cmd/gather/cluster/cluster.go @@ -1,3 +1,17 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package cluster import ( @@ -9,9 +23,6 @@ import ( "reflect" "strings" - otelv1alpha1 "github.com/open-telemetry/opentelemetry-operator/apis/v1alpha1" - otelv1beta1 "github.com/open-telemetry/opentelemetry-operator/apis/v1beta1" - "github.com/open-telemetry/opentelemetry-operator/cmd/gather/config" routev1 "github.com/openshift/api/route/v1" operatorsv1 "github.com/operator-framework/api/pkg/operators/v1" operatorsv1alpha1 "github.com/operator-framework/api/pkg/operators/v1alpha1" @@ -25,6 +36,10 @@ import ( "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime/schema" "sigs.k8s.io/controller-runtime/pkg/client" + + otelv1alpha1 "github.com/open-telemetry/opentelemetry-operator/apis/v1alpha1" + otelv1beta1 "github.com/open-telemetry/opentelemetry-operator/apis/v1beta1" + "github.com/open-telemetry/opentelemetry-operator/cmd/gather/config" ) type Cluster struct { @@ -141,6 +156,7 @@ func (c *Cluster) GetOLMInfo() error { return err } for _, o := range operators.Items { + o := o writeToFile(outputDir, &o) } @@ -154,6 +170,7 @@ func (c *Cluster) GetOLMInfo() error { return err } for _, o := range operatorGroups.Items { + o := o if strings.Contains(o.Name, "opentelemetry") { writeToFile(outputDir, &o) } @@ -168,6 +185,7 @@ func (c *Cluster) GetOLMInfo() error { return err } for _, o := range subscriptions.Items { + o := o writeToFile(outputDir, &o) } @@ -181,6 +199,7 @@ func (c *Cluster) GetOLMInfo() error { return err } for _, o := range ips.Items { + o := o writeToFile(outputDir, &o) } @@ -193,6 +212,7 @@ func (c *Cluster) GetOLMInfo() error { return err } for _, o := range csvs.Items { + o := o if strings.Contains(o.Name, "opentelemetry") { writeToFile(outputDir, &o) } @@ -214,6 +234,7 @@ func (c *Cluster) GetOpenTelemetryCollectors() error { errorDetected := false for _, otelCol := range otelCols.Items { + otelCol := otelCol err := c.processOTELCollector(&otelCol) if err != nil { log.Fatalln(err) @@ -240,6 +261,7 @@ func (c *Cluster) GetInstrumentations() error { errorDetected := false for _, instr := range instrumentations.Items { + instr := instr outputDir := filepath.Join(c.config.CollectionDir, instr.Namespace) err := os.MkdirAll(outputDir, os.ModePerm) if err != nil { diff --git a/cmd/gather/cluster/write.go b/cmd/gather/cluster/write.go index 762143fbb4..eafd05b795 100644 --- a/cmd/gather/cluster/write.go +++ b/cmd/gather/cluster/write.go @@ -1,3 +1,17 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package cluster import ( @@ -10,13 +24,14 @@ import ( "reflect" "strings" - "github.com/open-telemetry/opentelemetry-operator/apis/v1beta1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/serializer/json" cgocorev1 "k8s.io/client-go/kubernetes/typed/core/v1" "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/open-telemetry/opentelemetry-operator/apis/v1beta1" ) func createOTELFolder(collectionDir string, otelCol *v1beta1.OpenTelemetryCollector) (string, error) { diff --git a/cmd/gather/config/config.go b/cmd/gather/config/config.go index 0866c15b6f..481822b1be 100644 --- a/cmd/gather/config/config.go +++ b/cmd/gather/config/config.go @@ -1,3 +1,17 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package config import ( @@ -31,17 +45,17 @@ func NewConfig(scheme *runtime.Scheme) (Config, error) { config, err := clientcmd.BuildConfigFromFlags("", kubeconfigPath) if err != nil { - return Config{}, fmt.Errorf("Error reading the kubeconfig: %s\n", err.Error()) + return Config{}, fmt.Errorf("Error reading the kubeconfig: %w\n", err) } clusterClient, err := client.New(config, client.Options{Scheme: scheme}) if err != nil { - return Config{}, fmt.Errorf("Creating the Kubernetes client: %s\n", err) + return Config{}, fmt.Errorf("Creating the Kubernetes client: %w\n", err) } clientset, err := kubernetes.NewForConfig(config) if err != nil { - return Config{}, fmt.Errorf("Creating the Kubernetes clienset: %s\n", err) + return Config{}, fmt.Errorf("Creating the Kubernetes clienset: %w\n", err) } return Config{ diff --git a/cmd/gather/main.go b/cmd/gather/main.go index d4d0e24d3d..692bf14a38 100644 --- a/cmd/gather/main.go +++ b/cmd/gather/main.go @@ -1,13 +1,23 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package main import ( - "fmt" + "log" "os" - otelv1alpha1 "github.com/open-telemetry/opentelemetry-operator/apis/v1alpha1" - otelv1beta1 "github.com/open-telemetry/opentelemetry-operator/apis/v1beta1" - "github.com/open-telemetry/opentelemetry-operator/cmd/gather/cluster" - "github.com/open-telemetry/opentelemetry-operator/cmd/gather/config" routev1 "github.com/openshift/api/route/v1" operatorsv1 "github.com/operator-framework/api/pkg/operators/v1" operatorsv1alpha1 "github.com/operator-framework/api/pkg/operators/v1alpha1" @@ -20,6 +30,11 @@ import ( rbacv1 "k8s.io/api/rbac/v1" k8sruntime "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" + + otelv1alpha1 "github.com/open-telemetry/opentelemetry-operator/apis/v1alpha1" + otelv1beta1 "github.com/open-telemetry/opentelemetry-operator/apis/v1beta1" + "github.com/open-telemetry/opentelemetry-operator/cmd/gather/cluster" + "github.com/open-telemetry/opentelemetry-operator/cmd/gather/config" ) var scheme *k8sruntime.Scheme @@ -43,14 +58,29 @@ func init() { func main() { config, err := config.NewConfig(scheme) if err != nil { - fmt.Println(err) + log.Fatalln(err) os.Exit(1) } cluster := cluster.NewCluster(&config) - cluster.GetOperatorLogs() - cluster.GetOperatorDeploymentInfo() - cluster.GetOLMInfo() - cluster.GetOpenTelemetryCollectors() - cluster.GetInstrumentations() + err = cluster.GetOperatorLogs() + if err != nil { + log.Fatalln(err) + } + err = cluster.GetOperatorDeploymentInfo() + if err != nil { + log.Fatalln(err) + } + err = cluster.GetOLMInfo() + if err != nil { + log.Fatalln(err) + } + err = cluster.GetOpenTelemetryCollectors() + if err != nil { + log.Fatalln(err) + } + err = cluster.GetInstrumentations() + if err != nil { + log.Fatalln(err) + } } From d11a6df6370be80632f4797b40af4d532171562e Mon Sep 17 00:00:00 2001 From: Israel Blancas Date: Wed, 21 Aug 2024 17:49:31 +0200 Subject: [PATCH 06/14] Add documentation Signed-off-by: Israel Blancas --- .chloggen/3149-add-must-gather.yaml | 4 ++-- cmd/gather/README.md | 29 +++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 cmd/gather/README.md diff --git a/.chloggen/3149-add-must-gather.yaml b/.chloggen/3149-add-must-gather.yaml index b5a1deb601..19d6976ceb 100755 --- a/.chloggen/3149-add-must-gather.yaml +++ b/.chloggen/3149-add-must-gather.yaml @@ -5,7 +5,7 @@ change_type: enhancement component: operator # A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). -note: "Add a must gather application to help troubleshoot" +note: "Add a must gather utility to help troubleshoot" # One or more tracking issues related to the change issues: [3149] @@ -13,4 +13,4 @@ issues: [3149] # (Optional) One or more lines of additional information to render under the primary note. # These lines will be padded with 2 spaces and then inserted directly into the document. # Use pipe (|) for multiline entries. -subtext: "The new application was added to the operator container so it can be run without adding any new container. It can be run from the /must-gather path" \ No newline at end of file +subtext: "The new utility was added to the operator container so it can be run without adding any new container. It can be run from the /must-gather path" \ No newline at end of file diff --git a/cmd/gather/README.md b/cmd/gather/README.md new file mode 100644 index 0000000000..de849693ba --- /dev/null +++ b/cmd/gather/README.md @@ -0,0 +1,29 @@ +# OpenTelemetry Operator Must-Gather + +The OpenTelemetry Operator `must-gather` tool is designed to collect comprehensive information about OpenTelemetry components within an OpenShift cluster. +This utility extends the functionality of [OpenShift must-gather](https://github.com/openshift/must-gather) by specifically targeting and retrieving data related to the OpenTelemetry Operator, helping in diagnostics and troubleshooting. + +## What is a Must-Gather? + +The `must-gather` tool is a utility that collects logs, cluster information, and resource configurations related to a specific operator or application in an OpenShift cluster. It helps cluster administrators and developers diagnose issues by providing a snapshot of the cluster's state related to the targeted component. More information [in the official documentation](https://docs.openshift.com/container-platform/4.16/support/gathering-cluster-data.html). + +## Usage + +To run the must-gather tool for the OpenTelemetry Operator, use one of the following commands, depending on how you want to source the image and the namespace where the operator is deployed. + +### Using the image from the Operator deployment + +If you want to use the image directly from the existing OpenTelemetry Operator deployment, run the following command: + +```sh +oc adm must-gather --image=$(oc -n opentelemetry-operator-system get deployment.apps/opentelemetry-operator-controller-manager -o jsonpath='{.spec.template.spec.containers[?(@.name == "manager")].image}') -- /must-gather --namespace opentelemetry-operator-system +``` + +### Using the image from a local machine + +You can use the image in your machine with the following command: +```sh +docker run --entrypoint=/must-gather --help +``` + +Note that you can use this utility too to gather information about the objects deployed by the OpenTelemetry Operator if you don't use OpenShift. From 3c4ebd6bda9ce4fa8ce94c14c199f9ecf5d3e85f Mon Sep 17 00:00:00 2001 From: Israel Blancas Date: Tue, 27 Aug 2024 15:56:44 +0200 Subject: [PATCH 07/14] Clarify K8s compatibility Signed-off-by: Israel Blancas --- cmd/gather/README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/cmd/gather/README.md b/cmd/gather/README.md index de849693ba..eb42691148 100644 --- a/cmd/gather/README.md +++ b/cmd/gather/README.md @@ -1,7 +1,8 @@ # OpenTelemetry Operator Must-Gather -The OpenTelemetry Operator `must-gather` tool is designed to collect comprehensive information about OpenTelemetry components within an OpenShift cluster. -This utility extends the functionality of [OpenShift must-gather](https://github.com/openshift/must-gather) by specifically targeting and retrieving data related to the OpenTelemetry Operator, helping in diagnostics and troubleshooting. +The OpenTelemetry Operator `must-gather` tool is designed to collect comprehensive information about OpenTelemetry components within an OpenShift cluster. This utility extends the functionality of [OpenShift must-gather](https://github.com/openshift/must-gather) by specifically targeting and retrieving data related to the OpenTelemetry Operator, helping in diagnostics and troubleshooting. + +Note that you can use this utility too to gather information about the objects deployed by the OpenTelemetry Operator if you don't use OpenShift. ## What is a Must-Gather? @@ -26,4 +27,4 @@ You can use the image in your machine with the following command: docker run --entrypoint=/must-gather --help ``` -Note that you can use this utility too to gather information about the objects deployed by the OpenTelemetry Operator if you don't use OpenShift. +This is the recommended way to do it if you are not using OpenShift. \ No newline at end of file From eaddca1ce69cad7c68ae7f52658acb855c0c372c Mon Sep 17 00:00:00 2001 From: Israel Blancas Date: Wed, 28 Aug 2024 17:45:08 +0200 Subject: [PATCH 08/14] Some fixes Signed-off-by: Israel Blancas --- Dockerfile | 1 - Makefile | 15 +++++++++++++-- cmd/gather/Dockerfile | 16 ++++++++++++++++ cmd/gather/README.md | 16 +++++++++++----- cmd/gather/config/config.go | 21 +++++++++++++-------- 5 files changed, 53 insertions(+), 16 deletions(-) create mode 100644 cmd/gather/Dockerfile diff --git a/Dockerfile b/Dockerfile index 48104e6917..3ab8a0336d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,7 +15,6 @@ COPY --from=certificates /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-ce # Copy binary built on the host COPY bin/manager_${TARGETARCH} manager -COPY bin/must-gather_${TARGETARCH} must-gather USER 65532:65532 diff --git a/Makefile b/Makefile index b838216c56..29d54559d2 100644 --- a/Makefile +++ b/Makefile @@ -38,6 +38,8 @@ OPERATOROPAMPBRIDGE_IMG ?= ${IMG_PREFIX}/${OPERATOROPAMPBRIDGE_IMG_REPO}:$(addpr BRIDGETESTSERVER_IMG_REPO ?= e2e-test-app-bridge-server BRIDGETESTSERVER_IMG ?= ${IMG_PREFIX}/${BRIDGETESTSERVER_IMG_REPO}:ve2e +MUSTGATHER_IMG ?= ${IMG_PREFIX}/must-gather + # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) ifeq (,$(shell go env GOBIN)) GOBIN=$(shell go env GOPATH)/bin @@ -143,6 +145,7 @@ ci: generate fmt vet test ensure-generate-is-noop manager: generate CGO_ENABLED=0 GOOS=$(GOOS) GOARCH=$(ARCH) go build -o bin/manager_${ARCH} -ldflags "${COMMON_LDFLAGS} ${OPERATOR_LDFLAGS}" main.go +.PHONY: must-gather must-gather: CGO_ENABLED=0 GOOS=$(GOOS) GOARCH=$(ARCH) go build -o bin/must-gather_${ARCH} -ldflags "${COMMON_LDFLAGS} ${OPERATOR_LDFLAGS}" ./cmd/gather/main.go @@ -334,7 +337,7 @@ scorecard-tests: operator-sdk # buildx is used to ensure same results for arm based systems (m1/2 chips) .PHONY: container container: GOOS = linux -container: manager must-gather +container: manager docker build --load -t ${IMG} . # Push the container image, used only for local dev purposes @@ -365,6 +368,15 @@ container-bridge-test-server: GOOS = linux container-bridge-test-server: docker build --load -t ${BRIDGETESTSERVER_IMG} tests/test-e2e-apps/bridge-server +.PHONY: container-must-gather +container-must-gather: GOOS = linux +container-must-gather: must-gather + docker build -f cmd/gather/Dockerfile --load -t ${MUSTGATHER_IMG} . + +.PHONY: container-must-gather-push +container-must-gather-push: + docker push ${MUSTGATHER_IMG} + .PHONY: start-kind start-kind: kind ifeq (true,$(START_KIND_CLUSTER)) @@ -391,7 +403,6 @@ else $(MAKE) container-push endif - .PHONY: load-image-target-allocator load-image-target-allocator: container-target-allocator kind ifeq (true,$(START_KIND_CLUSTER)) diff --git a/cmd/gather/Dockerfile b/cmd/gather/Dockerfile new file mode 100644 index 0000000000..3eb3d5c54b --- /dev/null +++ b/cmd/gather/Dockerfile @@ -0,0 +1,16 @@ +FROM registry.access.redhat.com/ubi9-minimal:9.2 + +RUN INSTALL_PKGS=" \ + rsync \ + tar \ + " && \ + microdnf install -y $INSTALL_PKGS && \ + microdnf clean all +WORKDIR / + +ARG TARGETARCH +COPY bin/must-gather_${TARGETARCH} /usr/bin/must-gather + +USER 65532:65532 + +ENTRYPOINT ["/usr/bin/must-gather"] diff --git a/cmd/gather/README.md b/cmd/gather/README.md index eb42691148..84b7c04eca 100644 --- a/cmd/gather/README.md +++ b/cmd/gather/README.md @@ -10,21 +10,27 @@ The `must-gather` tool is a utility that collects logs, cluster information, and ## Usage +First, you will need to build and push the image: +```sh +make container-must-gather container-must-gather-push +``` + To run the must-gather tool for the OpenTelemetry Operator, use one of the following commands, depending on how you want to source the image and the namespace where the operator is deployed. ### Using the image from the Operator deployment -If you want to use the image directly from the existing OpenTelemetry Operator deployment, run the following command: +If you want to use the image in a running cluster, you need to run the following command: ```sh -oc adm must-gather --image=$(oc -n opentelemetry-operator-system get deployment.apps/opentelemetry-operator-controller-manager -o jsonpath='{.spec.template.spec.containers[?(@.name == "manager")].image}') -- /must-gather --namespace opentelemetry-operator-system +oc adm must-gather --image= -- /usr/bin/must-gather --operator-namespace opentelemetry-operator-system ``` -### Using the image from a local machine +### Using it as a CLI -You can use the image in your machine with the following command: +You only need to build and run: ```sh -docker run --entrypoint=/must-gather --help +make must-gather +./bin/must-gather --help ``` This is the recommended way to do it if you are not using OpenShift. \ No newline at end of file diff --git a/cmd/gather/config/config.go b/cmd/gather/config/config.go index 481822b1be..9c426a4e52 100644 --- a/cmd/gather/config/config.go +++ b/cmd/gather/config/config.go @@ -21,6 +21,7 @@ import ( "github.com/spf13/pflag" "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" "k8s.io/client-go/util/homedir" "sigs.k8s.io/controller-runtime/pkg/client" @@ -35,27 +36,31 @@ type Config struct { } func NewConfig(scheme *runtime.Scheme) (Config, error) { - var operatorName, operatorNamespace, kubeconfigPath, collectionDir string + var operatorName, operatorNamespace, collectionDir string pflag.StringVar(&operatorName, "operator-name", "opentelemetry-operator", "Operator name") pflag.StringVar(&operatorNamespace, "operator-namespace", "", "Namespace where the operator was deployed") - pflag.StringVar(&kubeconfigPath, "kubeconfig-path", filepath.Join(homedir.HomeDir(), ".kube", "config"), "Absolute path to the KubeconfigPath file") - pflag.StringVar(&collectionDir, "collection-dir", filepath.Join(homedir.HomeDir(), "must-gather"), "Absolute path to the KubeconfigPath file") + pflag.StringVar(&collectionDir, "collection-dir", filepath.Join(homedir.HomeDir(), "/must-gather"), "Absolute path to the KubeconfigPath file") pflag.Parse() - config, err := clientcmd.BuildConfigFromFlags("", kubeconfigPath) - if err != nil { - return Config{}, fmt.Errorf("Error reading the kubeconfig: %w\n", err) + var config *rest.Config + config, err1 := rest.InClusterConfig() + if err1 != nil { + var err2 error + config, err2 = clientcmd.BuildConfigFromFlags("", "") + if err2 != nil { + return Config{}, fmt.Errorf("it was not possible to connect to connecto Kubernetes: [%w, %w]", err1, err2) + } } clusterClient, err := client.New(config, client.Options{Scheme: scheme}) if err != nil { - return Config{}, fmt.Errorf("Creating the Kubernetes client: %w\n", err) + return Config{}, fmt.Errorf("creating the Kubernetes client: %w\n", err) } clientset, err := kubernetes.NewForConfig(config) if err != nil { - return Config{}, fmt.Errorf("Creating the Kubernetes clienset: %w\n", err) + return Config{}, fmt.Errorf("creating the Kubernetes clienset: %w\n", err) } return Config{ From abfdacf6560649d058e20ca4c74de1cefa822dfe Mon Sep 17 00:00:00 2001 From: Israel Blancas Date: Fri, 30 Aug 2024 11:32:04 +0200 Subject: [PATCH 09/14] Publish image Signed-off-by: Israel Blancas --- .github/workflows/publish-images.yaml | 2 +- .github/workflows/publish-must-gather.yaml | 92 ++++++++++++++++++++++ Makefile | 2 +- 3 files changed, 94 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/publish-must-gather.yaml diff --git a/.github/workflows/publish-images.yaml b/.github/workflows/publish-images.yaml index 3c705c6e9e..3c5fa49108 100644 --- a/.github/workflows/publish-images.yaml +++ b/.github/workflows/publish-images.yaml @@ -47,7 +47,7 @@ jobs: for platform in $(echo $PLATFORMS | tr "," "\n"); do arch=${platform#*/} echo "Building manager for $arch" - make manager must-gather ARCH=$arch + make manager ARCH=$arch done - name: Docker meta diff --git a/.github/workflows/publish-must-gather.yaml b/.github/workflows/publish-must-gather.yaml new file mode 100644 index 0000000000..5168ffb129 --- /dev/null +++ b/.github/workflows/publish-must-gather.yaml @@ -0,0 +1,92 @@ +name: "Publish Python Auto-Instrumentation" + +on: + push: + branches: [ main ] + tags: [ 'v*' ] + + workflow_dispatch: + +env: + PLATFORMS: linux/amd64,linux/arm64,linux/s390x,linux/ppc64le + +jobs: + publish: + name: Publish must-gather container image + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version: '~1.22.4' + + - name: Unshallow + run: git fetch --prune --unshallow + + - name: Set env vars for the job + run: | + echo "VERSION_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_ENV + echo "VERSION=$(git describe --tags | sed 's/^v//')" >> $GITHUB_ENV + + - name: Build the binary for each supported architecture + run: | + for platform in $(echo $PLATFORMS | tr "," "\n"); do + arch=${platform#*/} + echo "Building must-gather for $arch" + make must-gather ARCH=$arch + done + + - name: Docker meta + id: docker_meta + uses: docker/metadata-action@v5 + with: + images: | + otel/opentelemetry-operator-must-gather + ghcr.io/open-telemetry/opentelemetry-operator/must-gather + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{raw}} + type=ref,event=branch + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Cache Docker layers + uses: actions/cache@v4 + with: + path: /tmp/.buildx-cache + key: ${{ runner.os }}-buildx-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-buildx- + + - name: Log into Docker.io + uses: docker/login-action@v3 + if: ${{ github.event_name == 'push' }} + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Login to GitHub Package Registry + uses: docker/login-action@v3 + if: ${{ github.event_name == 'push' }} + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push must-gather image + uses: docker/build-push-action@v6 + with: + context: . + file: ./cmd/gather/Dockerfile + platforms: ${{ env.PLATFORMS }} + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.docker_meta.outputs.tags }} + labels: ${{ steps.docker_meta.outputs.labels }} + cache-from: type=local,src=/tmp/.buildx-cache + cache-to: type=local,dest=/tmp/.buildx-cache diff --git a/Makefile b/Makefile index 29d54559d2..a66ab0ee6a 100644 --- a/Makefile +++ b/Makefile @@ -147,7 +147,7 @@ manager: generate .PHONY: must-gather must-gather: - CGO_ENABLED=0 GOOS=$(GOOS) GOARCH=$(ARCH) go build -o bin/must-gather_${ARCH} -ldflags "${COMMON_LDFLAGS} ${OPERATOR_LDFLAGS}" ./cmd/gather/main.go + CGO_ENABLED=0 GOOS=$(GOOS) GOARCH=$(ARCH) go build -o bin/must-gather_${ARCH} -ldflags "${COMMON_LDFLAGS}" ./cmd/gather/main.go # Build target allocator binary .PHONY: targetallocator From f458b9bd5098a55ea27a14f476c85d598c25e605 Mon Sep 17 00:00:00 2001 From: Israel Blancas Date: Mon, 9 Sep 2024 10:55:09 +0200 Subject: [PATCH 10/14] Fix release notes Signed-off-by: Israel Blancas --- .chloggen/3149-add-must-gather.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.chloggen/3149-add-must-gather.yaml b/.chloggen/3149-add-must-gather.yaml index 19d6976ceb..3e082e2ca9 100755 --- a/.chloggen/3149-add-must-gather.yaml +++ b/.chloggen/3149-add-must-gather.yaml @@ -13,4 +13,4 @@ issues: [3149] # (Optional) One or more lines of additional information to render under the primary note. # These lines will be padded with 2 spaces and then inserted directly into the document. # Use pipe (|) for multiline entries. -subtext: "The new utility was added to the operator container so it can be run without adding any new container. It can be run from the /must-gather path" \ No newline at end of file +subtext: "The new utility is available as part of a new container image" From 8fa48a7401c380aa48ff5c8bf03ef2dad52c05c0 Mon Sep 17 00:00:00 2001 From: Israel Blancas Date: Thu, 19 Sep 2024 15:02:56 +0200 Subject: [PATCH 11/14] Small improvements Signed-off-by: Israel Blancas --- cmd/gather/cluster/cluster.go | 290 ++++++++++------------------------ cmd/gather/config/config.go | 18 ++- 2 files changed, 95 insertions(+), 213 deletions(-) diff --git a/cmd/gather/cluster/cluster.go b/cmd/gather/cluster/cluster.go index e6299833d8..3d289fb6ae 100644 --- a/cmd/gather/cluster/cluster.go +++ b/cmd/gather/cluster/cluster.go @@ -35,6 +35,7 @@ import ( rbacv1 "k8s.io/api/rbac/v1" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" otelv1alpha1 "github.com/open-telemetry/opentelemetry-operator/apis/v1alpha1" @@ -43,13 +44,16 @@ import ( ) type Cluster struct { - config *config.Config + config *config.Config + apiAvailabilityCache map[schema.GroupVersionResource]bool } func NewCluster(cfg *config.Config) Cluster { - return Cluster{config: cfg} + return Cluster{ + config: cfg, + apiAvailabilityCache: make(map[schema.GroupVersionResource]bool), + } } - func (c *Cluster) getOperatorNamespace() (string, error) { if c.config.OperatorNamespace != "" { return c.config.OperatorNamespace, nil @@ -295,207 +299,42 @@ func (c *Cluster) processOTELCollector(otelCol *otelv1beta1.OpenTelemetryCollect return nil } -func (c *Cluster) processOwnedResources(otelCol *otelv1beta1.OpenTelemetryCollector, folder string) error { - errorDetected := false - - ////////////////////////////////////////////////////////////////// apps/v1 - // DaemonSets - daemonsets, err := c.getOwnerResources(&appsv1.DaemonSetList{}, otelCol) - if err != nil { - errorDetected = true - log.Fatalln(err) - } - for _, d := range daemonsets { - writeToFile(folder, d) - } - - // Deployments - deployments, err := c.getOwnerResources(&appsv1.DeploymentList{}, otelCol) - if err != nil { - errorDetected = true - log.Fatalln(err) - } - for _, d := range deployments { - writeToFile(folder, d) - } - - // StatefulSets - statefulsets, err := c.getOwnerResources(&appsv1.StatefulSetList{}, otelCol) - if err != nil { - errorDetected = true - log.Fatalln(err) - } - for _, s := range statefulsets { - writeToFile(folder, s) - } - - ////////////////////////////////////////////////////////////////// rbac/v1 - // ClusterRole - crs, err := c.getOwnerResources(&rbacv1.ClusterRoleList{}, otelCol) - if err != nil { - errorDetected = true - log.Fatalln(err) - } - for _, cr := range crs { - writeToFile(folder, cr) - } - - // ClusterRoleBindings - crbs, err := c.getOwnerResources(&rbacv1.ClusterRoleBindingList{}, otelCol) - if err != nil { - errorDetected = true - log.Fatalln(err) - } - for _, crb := range crbs { - writeToFile(folder, crb) - } - - ////////////////////////////////////////////////////////////////// core/v1 - // ConfigMaps - cms, err := c.getOwnerResources(&corev1.ConfigMapList{}, otelCol) - if err != nil { - errorDetected = true - log.Fatalln(err) - } - for _, c := range cms { - writeToFile(folder, c) - } - - // PersistentVolumes - pvs, err := c.getOwnerResources(&corev1.PersistentVolumeList{}, otelCol) - if err != nil { - errorDetected = true - log.Fatalln(err) - } - for _, p := range pvs { - writeToFile(folder, p) - } - - // PersistentVolumeClaims - pvcs, err := c.getOwnerResources(&corev1.PersistentVolumeClaimList{}, otelCol) - if err != nil { - errorDetected = true - log.Fatalln(err) - } - for _, p := range pvcs { - writeToFile(folder, p) - } - - // Pods - pods, err := c.getOwnerResources(&corev1.PodList{}, otelCol) - if err != nil { - errorDetected = true - log.Fatalln(err) - } - for _, p := range pods { - writeToFile(folder, p) - } - - // Services - services, err := c.getOwnerResources(&corev1.ServiceList{}, otelCol) - if err != nil { - errorDetected = true - log.Fatalln(err) - } - for _, s := range services { - writeToFile(folder, s) - } - - // ServiceAccounts - sas, err := c.getOwnerResources(&corev1.ServiceAccountList{}, otelCol) - if err != nil { - errorDetected = true - log.Fatalln(err) - } - for _, s := range sas { - writeToFile(folder, s) - } - - ////////////////////////////////////////////////////////////////// autoscaling/v2 - // HPAs - hpas, err := c.getOwnerResources(&autoscalingv2.HorizontalPodAutoscalerList{}, otelCol) - if err != nil { - errorDetected = true - log.Fatalln(err) - } - for _, h := range hpas { - writeToFile(folder, h) - } - - ////////////////////////////////////////////////////////////////// networking/v1 - // Ingresses - ingresses, err := c.getOwnerResources(&networkingv1.IngressList{}, otelCol) - if err != nil { - errorDetected = true - log.Fatalln(err) - } - for _, i := range ingresses { - writeToFile(folder, i) - } - - ////////////////////////////////////////////////////////////////// policy/v1 - // PodDisruptionBudge - pdbs, err := c.getOwnerResources(&policy1.PodDisruptionBudgetList{}, otelCol) - if err != nil { - errorDetected = true - log.Fatalln(err) - } - for _, pdb := range pdbs { - writeToFile(folder, pdb) - } - - ////////////////////////////////////////////////////////////////// monitoring/v1 - if c.isAPIAvailable(schema.GroupVersionResource{ - Group: monitoringv1.SchemeGroupVersion.Group, - Version: monitoringv1.SchemeGroupVersion.Version, - Resource: "ServiceMonitor", - }) { - // PodMonitors - pms, err := c.getOwnerResources(&monitoringv1.PodMonitorList{}, otelCol) - if err != nil { - errorDetected = true - log.Fatalln(err) - } - for _, pm := range pms { - writeToFile(folder, pm) - } - - // ServiceMonitors - sms, err := c.getOwnerResources(&monitoringv1.ServiceMonitorList{}, otelCol) - if err != nil { - errorDetected = true - log.Fatalln(err) - } - for _, s := range sms { - writeToFile(folder, s) - } - } - - ////////////////////////////////////////////////////////////////// route/v1 - // Routes - if c.isAPIAvailable(schema.GroupVersionResource{ - Group: routev1.GroupName, - Version: routev1.GroupVersion.Version, - Resource: "Route", - }) { - rs, err := c.getOwnerResources(&routev1.RouteList{}, otelCol) - if err != nil { - errorDetected = true - log.Fatalln(err) - } - for _, r := range rs { - writeToFile(folder, r) +func (c *Cluster) processOwnedResources(owner interface{}, folder string) error { + resourceTypes := []struct { + list client.ObjectList + apiCheck func() bool + }{ + {&appsv1.DaemonSetList{}, func() bool { return true }}, + {&appsv1.DeploymentList{}, func() bool { return true }}, + {&appsv1.StatefulSetList{}, func() bool { return true }}, + {&rbacv1.ClusterRoleList{}, func() bool { return true }}, + {&rbacv1.ClusterRoleBindingList{}, func() bool { return true }}, + {&corev1.ConfigMapList{}, func() bool { return true }}, + {&corev1.PersistentVolumeList{}, func() bool { return true }}, + {&corev1.PersistentVolumeClaimList{}, func() bool { return true }}, + {&corev1.PodList{}, func() bool { return true }}, + {&corev1.ServiceList{}, func() bool { return true }}, + {&corev1.ServiceAccountList{}, func() bool { return true }}, + {&autoscalingv2.HorizontalPodAutoscalerList{}, func() bool { return true }}, + {&networkingv1.IngressList{}, func() bool { return true }}, + {&policy1.PodDisruptionBudgetList{}, func() bool { return true }}, + {&monitoringv1.PodMonitorList{}, c.isMonitoringAPIAvailable}, + {&monitoringv1.ServiceMonitorList{}, c.isMonitoringAPIAvailable}, + {&routev1.RouteList{}, c.isRouteAPIAvailable}, + } + + for _, rt := range resourceTypes { + if rt.apiCheck() { + if err := c.processResourceType(rt.list, owner, folder); err != nil { + return err + } } } - if errorDetected { - return fmt.Errorf("something failed while getting the associated resources") - } - return nil } -func (c *Cluster) getOwnerResources(objList client.ObjectList, otelCol *otelv1beta1.OpenTelemetryCollector) ([]client.Object, error) { +func (c *Cluster) getOwnerResources(objList client.ObjectList, owner interface{}) ([]client.Object, error) { err := c.config.KubernetesClient.List(context.TODO(), objList, &client.ListOptions{ LabelSelector: labels.SelectorFromSet(labels.Set{ "app.kubernetes.io/managed-by": "opentelemetry-operator", @@ -510,7 +349,7 @@ func (c *Cluster) getOwnerResources(objList client.ObjectList, otelCol *otelv1be items := reflect.ValueOf(objList).Elem().FieldByName("Items") for i := 0; i < items.Len(); i++ { item := items.Index(i).Addr().Interface().(client.Object) - if hasOwnerReference(item, otelCol) { + if hasOwnerReference(item, owner) { resources = append(resources, item) } } @@ -518,20 +357,61 @@ func (c *Cluster) getOwnerResources(objList client.ObjectList, otelCol *otelv1be } +func (c *Cluster) processResourceType(list client.ObjectList, owner interface{}, folder string) error { + resources, err := c.getOwnerResources(list, owner) + if err != nil { + return fmt.Errorf("failed to get resources: %w", err) + } + for _, resource := range resources { + writeToFile(folder, resource) + } + return nil +} + +func (c *Cluster) isMonitoringAPIAvailable() bool { + return c.isAPIAvailable(schema.GroupVersionResource{ + Group: monitoringv1.SchemeGroupVersion.Group, + Version: monitoringv1.SchemeGroupVersion.Version, + Resource: "ServiceMonitor", + }) +} + +func (c *Cluster) isRouteAPIAvailable() bool { + return c.isAPIAvailable(schema.GroupVersionResource{ + Group: routev1.GroupName, + Version: routev1.GroupVersion.Version, + Resource: "Route", + }) +} + func (c *Cluster) isAPIAvailable(gvr schema.GroupVersionResource) bool { + if result, ok := c.apiAvailabilityCache[gvr]; ok { + return result + } + rm := c.config.KubernetesClient.RESTMapper() gvk, err := rm.KindFor(gvr) - if err != nil { - return false - } + result := err == nil && !gvk.Empty() + c.apiAvailabilityCache[gvr] = result - return !gvk.Empty() + return result } -func hasOwnerReference(obj client.Object, otelCol *otelv1beta1.OpenTelemetryCollector) bool { +func hasOwnerReference(obj client.Object, owner interface{}) bool { + var ownerKind string + var ownerUID types.UID + + switch o := owner.(type) { + case *otelv1beta1.OpenTelemetryCollector: + ownerKind = o.Kind + ownerUID = o.UID + default: + return false + } + for _, ownerRef := range obj.GetOwnerReferences() { - if ownerRef.Kind == otelCol.Kind && ownerRef.UID == otelCol.UID { + if ownerRef.Kind == ownerKind && ownerRef.UID == ownerUID { return true } } diff --git a/cmd/gather/config/config.go b/cmd/gather/config/config.go index 9c426a4e52..4a1ba48ff0 100644 --- a/cmd/gather/config/config.go +++ b/cmd/gather/config/config.go @@ -36,20 +36,22 @@ type Config struct { } func NewConfig(scheme *runtime.Scheme) (Config, error) { - var operatorName, operatorNamespace, collectionDir string + var operatorName, operatorNamespace, collectionDir, kubeconfigPath string pflag.StringVar(&operatorName, "operator-name", "opentelemetry-operator", "Operator name") pflag.StringVar(&operatorNamespace, "operator-namespace", "", "Namespace where the operator was deployed") pflag.StringVar(&collectionDir, "collection-dir", filepath.Join(homedir.HomeDir(), "/must-gather"), "Absolute path to the KubeconfigPath file") + pflag.StringVar(&kubeconfigPath, "kubeconfig", "", "Path to the kubeconfig file") pflag.Parse() - var config *rest.Config - config, err1 := rest.InClusterConfig() - if err1 != nil { - var err2 error - config, err2 = clientcmd.BuildConfigFromFlags("", "") - if err2 != nil { - return Config{}, fmt.Errorf("it was not possible to connect to connecto Kubernetes: [%w, %w]", err1, err2) + config, err := rest.InClusterConfig() + if err != nil { + if kubeconfigPath == "" { + kubeconfigPath = filepath.Join(homedir.HomeDir(), ".kube", "config") + } + config, err = clientcmd.BuildConfigFromFlags("", kubeconfigPath) + if err != nil { + return Config{}, fmt.Errorf("failed to create Kubernetes client config: %w", err) } } From c7dc6b6250ab9025a386e970b6a35682329cfffc Mon Sep 17 00:00:00 2001 From: Israel Blancas Date: Thu, 26 Sep 2024 13:45:33 +0200 Subject: [PATCH 12/14] Add unit tests Signed-off-by: Israel Blancas --- cmd/gather/cluster/cluster_test.go | 176 +++++++++++++++++++ cmd/gather/cluster/write_test.go | 261 +++++++++++++++++++++++++++++ go.mod | 1 + 3 files changed, 438 insertions(+) create mode 100644 cmd/gather/cluster/cluster_test.go create mode 100644 cmd/gather/cluster/write_test.go diff --git a/cmd/gather/cluster/cluster_test.go b/cmd/gather/cluster/cluster_test.go new file mode 100644 index 0000000000..8749d688e5 --- /dev/null +++ b/cmd/gather/cluster/cluster_test.go @@ -0,0 +1,176 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cluster + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + appsv1 "k8s.io/api/apps/v1" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/open-telemetry/opentelemetry-operator/cmd/gather/config" +) + +// MockClient is a mock implementation of client.Client. +type MockClient struct { + mock.Mock +} + +func (m *MockClient) Get(ctx context.Context, key types.NamespacedName, obj client.Object, _ ...client.GetOption) error { + args := m.Called(ctx, key, obj) + return args.Error(0) +} + +func (m *MockClient) List(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error { + args := m.Called(ctx, list, opts) + return args.Error(0) +} + +func (m *MockClient) Create(ctx context.Context, obj client.Object, opts ...client.CreateOption) error { + args := m.Called(ctx, obj, opts) + return args.Error(0) +} + +func (m *MockClient) Delete(ctx context.Context, obj client.Object, opts ...client.DeleteOption) error { + args := m.Called(ctx, obj, opts) + return args.Error(0) +} + +func (m *MockClient) Update(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error { + args := m.Called(ctx, obj, opts) + return args.Error(0) +} + +func (m *MockClient) Patch(ctx context.Context, obj client.Object, patch client.Patch, opts ...client.PatchOption) error { + args := m.Called(ctx, obj, patch, opts) + return args.Error(0) +} + +func (m *MockClient) DeleteAllOf(ctx context.Context, obj client.Object, opts ...client.DeleteAllOfOption) error { + args := m.Called(ctx, obj, opts) + return args.Error(0) +} + +func (m *MockClient) Status() client.StatusWriter { + args := m.Called() + return args.Get(0).(client.StatusWriter) +} + +func (m *MockClient) Scheme() *runtime.Scheme { + args := m.Called() + return args.Get(0).(*runtime.Scheme) +} + +func (m *MockClient) RESTMapper() meta.RESTMapper { + args := m.Called() + return args.Get(0).(meta.RESTMapper) +} + +func (m *MockClient) GroupVersionKindFor(obj runtime.Object) (schema.GroupVersionKind, error) { + return schema.GroupVersionKind{}, nil +} + +func (m *MockClient) IsObjectNamespaced(_ runtime.Object) (bool, error) { + return true, nil +} + +func (m *MockClient) SubResource(string) client.SubResourceClient { + return nil +} + +func TestGetOperatorNamespace(t *testing.T) { + mockClient := new(MockClient) + cfg := &config.Config{ + KubernetesClient: mockClient, + } + cluster := NewCluster(cfg) + + // Test when OperatorNamespace is already set + cfg.OperatorNamespace = "test-namespace" + ns, err := cluster.getOperatorNamespace() + assert.NoError(t, err) + assert.Equal(t, "test-namespace", ns) + + // Test when OperatorNamespace is not set + cfg.OperatorNamespace = "" + mockClient.On("List", mock.Anything, &appsv1.DeploymentList{}, mock.Anything).Return(nil).Run(func(args mock.Arguments) { + arg := args.Get(1).(*appsv1.DeploymentList) + arg.Items = []appsv1.Deployment{ + { + ObjectMeta: metav1.ObjectMeta{ + Namespace: "operator-namespace", + }, + }, + } + }) + + ns, err = cluster.getOperatorNamespace() + assert.NoError(t, err) + assert.Equal(t, "operator-namespace", ns) + mockClient.AssertExpectations(t) +} + +func TestGetOperatorDeployment(t *testing.T) { + + mockClient := new(MockClient) + cfg := &config.Config{ + KubernetesClient: mockClient, + } + cluster := NewCluster(cfg) + // Test successful case + mockClient.On("List", mock.Anything, &appsv1.DeploymentList{}, mock.Anything).Return(nil).Run(func(args mock.Arguments) { + arg := args.Get(1).(*appsv1.DeploymentList) + arg.Items = []appsv1.Deployment{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "opentelemetry-operator", + }, + }, + } + }) + + deployment, err := cluster.getOperatorDeployment() + assert.NoError(t, err) + assert.Equal(t, "opentelemetry-operator", deployment.Name) + + mockClient.AssertExpectations(t) +} + +func TestGetOperatorDeploymentNotFound(t *testing.T) { + + mockClient := new(MockClient) + cfg := &config.Config{ + KubernetesClient: mockClient, + } + cluster := NewCluster(cfg) + + // Test when no operator is found + mockClient.On("List", mock.Anything, &appsv1.DeploymentList{}, mock.Anything).Return(nil).Run(func(args mock.Arguments) { + arg := args.Get(1).(*appsv1.DeploymentList) + arg.Items = []appsv1.Deployment{} + }) + + _, err := cluster.getOperatorDeployment() + assert.Error(t, err) + assert.Equal(t, "operator not found", err.Error()) +} diff --git a/cmd/gather/cluster/write_test.go b/cmd/gather/cluster/write_test.go new file mode 100644 index 0000000000..262931abb3 --- /dev/null +++ b/cmd/gather/cluster/write_test.go @@ -0,0 +1,261 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cluster + +import ( + "context" + "io" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/rest" + + "github.com/open-telemetry/opentelemetry-operator/apis/v1beta1" +) + +type MockObject struct { + mock.Mock +} + +// Implement all methods required by client.Object and runtime.Object + +// GetObjectKind mocks the GetObjectKind method. +func (m *MockObject) GetObjectKind() schema.ObjectKind { + args := m.Called() + return args.Get(0).(schema.ObjectKind) +} + +// GetName mocks the GetName method. +func (m *MockObject) GetName() string { + args := m.Called() + return args.String(0) +} + +// SetName mocks the SetName method. +func (m *MockObject) SetName(name string) { + m.Called(name) +} + +// GetNamespace mocks the GetNamespace method. +func (m *MockObject) GetNamespace() string { + args := m.Called() + return args.String(0) +} + +// SetNamespace mocks the SetNamespace method. +func (m *MockObject) SetNamespace(namespace string) { + m.Called(namespace) +} + +// GetAnnotations mocks the GetAnnotations method. +func (m *MockObject) GetAnnotations() map[string]string { + args := m.Called() + return args.Get(0).(map[string]string) +} + +// SetAnnotations mocks the SetAnnotations method. +func (m *MockObject) SetAnnotations(annotations map[string]string) { + m.Called(annotations) +} + +// GetCreationTimestamp mocks the GetCreationTimestamp method. +func (m *MockObject) GetCreationTimestamp() v1.Time { + args := m.Called() + return args.Get(0).(v1.Time) +} + +// SetCreationTimestamp mocks the SetCreationTimestamp method. +func (m *MockObject) SetCreationTimestamp(timestamp v1.Time) { + m.Called(timestamp) +} + +// GetDeletionGracePeriodSeconds mocks the GetDeletionGracePeriodSeconds method. +func (m *MockObject) GetDeletionGracePeriodSeconds() *int64 { + args := m.Called() + return args.Get(0).(*int64) +} + +// GetDeletionTimestamp mocks the GetDeletionTimestamp method. +func (m *MockObject) GetDeletionTimestamp() *v1.Time { + args := m.Called() + return args.Get(0).(*v1.Time) +} + +// GetLabels mocks the GetLabels method. +func (m *MockObject) GetLabels() map[string]string { + args := m.Called() + return args.Get(0).(map[string]string) +} + +// SetLabels mocks the SetLabels method. +func (m *MockObject) SetLabels(labels map[string]string) { + m.Called(labels) +} + +// GetFinalizers mocks the GetFinalizers method. +func (m *MockObject) GetFinalizers() []string { + args := m.Called() + return args.Get(0).([]string) +} + +// SetFinalizers mocks the SetFinalizers method. +func (m *MockObject) SetFinalizers(finalizers []string) { + m.Called(finalizers) +} + +// GetGenerateName mocks the GetGenerateName method. +func (m *MockObject) GetGenerateName() string { + args := m.Called() + return args.String(0) +} + +// SetGenerateName mocks the SetGenerateName method. +func (m *MockObject) SetGenerateName(name string) { + m.Called(name) +} + +// DeepCopyObject mocks the DeepCopyObject method. +func (m *MockObject) DeepCopyObject() runtime.Object { + args := m.Called() + return args.Get(0).(runtime.Object) +} + +func (m *MockObject) GetManagedFields() []v1.ManagedFieldsEntry { + args := m.Called() + return args.Get(0).([]v1.ManagedFieldsEntry) +} + +func (m *MockObject) GetOwnerReferences() []v1.OwnerReference { + args := m.Called() + return args.Get(0).([]v1.OwnerReference) +} + +func (m *MockObject) GetGeneration() int64 { + args := m.Called() + return args.Get(0).(int64) +} + +func (m *MockObject) GetResourceVersion() string { + args := m.Called() + return args.String(0) +} + +func (m *MockObject) GetSelfLink() string { + args := m.Called() + return args.String(0) +} + +type MockPodInterface struct { + mock.Mock +} + +func (m *MockPodInterface) GetLogs(podName string, options *corev1.PodLogOptions) *rest.Request { + args := m.Called(podName, options) + return args.Get(0).(*rest.Request) +} + +type MockRequest struct { + mock.Mock +} + +func (m *MockRequest) Stream(ctx context.Context) (io.ReadCloser, error) { + args := m.Called(ctx) + return args.Get(0).(io.ReadCloser), args.Error(1) +} + +func TestCreateOTELFolder(t *testing.T) { + collectionDir := "/tmp/test-dir" + otelCol := &v1beta1.OpenTelemetryCollector{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test-namespace", + Name: "test-otel", + }, + } + + outputDir, err := createOTELFolder(collectionDir, otelCol) + + expectedDir := filepath.Join(collectionDir, "namespaces", otelCol.Namespace, otelCol.Name) + assert.NoError(t, err) + assert.Equal(t, expectedDir, outputDir) + + // Clean up after the test + os.RemoveAll(collectionDir) +} + +func TestCreateFile(t *testing.T) { + outputDir := "/tmp/test-dir" + err := os.MkdirAll(outputDir, os.ModePerm) + assert.NoError(t, err) + defer os.RemoveAll(outputDir) + + mockObj := &MockObject{} + mockObj.On("GetObjectKind").Return(schema.EmptyObjectKind) + mockObj.On("GetName").Return("test-deployment") + mockObj.On("DeepCopyObject").Return(mockObj) + + file, err := createFile(outputDir, mockObj) + assert.NoError(t, err) + defer file.Close() + + expectedPath := filepath.Join(outputDir, "mockobject-test-deployment.yaml") + _, err = os.Stat(expectedPath) + assert.NoError(t, err) +} + +func (m *MockObject) SetUID(uid types.UID) { + m.Called(uid) +} + +func (m *MockObject) GetUID() types.UID { + args := m.Called() + return args.Get(0).(types.UID) +} + +func (m *MockObject) SetDeletionGracePeriodSeconds(seconds *int64) { + m.Called(seconds) +} + +func (m *MockObject) SetDeletionTimestamp(timestamp *v1.Time) { + m.Called(timestamp) +} + +func (m *MockObject) SetGeneration(generation int64) { + m.Called(generation) +} + +func (m *MockObject) SetManagedFields(fields []v1.ManagedFieldsEntry) { + m.Called(fields) +} + +func (m *MockObject) SetOwnerReferences(references []v1.OwnerReference) { + m.Called(references) +} + +func (m *MockObject) SetResourceVersion(version string) { + m.Called(version) +} + +func (m *MockObject) SetSelfLink(selfLink string) { + m.Called(selfLink) +} diff --git a/go.mod b/go.mod index 10d8cc64aa..ff32be3790 100644 --- a/go.mod +++ b/go.mod @@ -187,6 +187,7 @@ require ( github.com/scaleway/scaleway-sdk-go v1.0.0-beta.29 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/spf13/cobra v1.8.1 // indirect + github.com/stretchr/objx v0.5.2 // indirect github.com/tklauser/go-sysconf v0.3.13 // indirect github.com/tklauser/numcpus v0.7.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect From d83ead8a7b561cf1d0ef01d69b2dd1f5af4d36be Mon Sep 17 00:00:00 2001 From: Israel Blancas Date: Wed, 2 Oct 2024 18:00:43 +0200 Subject: [PATCH 13/14] Apply changes requested in code review Signed-off-by: Israel Blancas --- .chloggen/3149-add-must-gather.yaml | 13 +++++++++++-- .github/workflows/publish-must-gather.yaml | 8 +------- cmd/gather/README.md | 8 ++++---- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/.chloggen/3149-add-must-gather.yaml b/.chloggen/3149-add-must-gather.yaml index 3e082e2ca9..12affefa8c 100755 --- a/.chloggen/3149-add-must-gather.yaml +++ b/.chloggen/3149-add-must-gather.yaml @@ -2,7 +2,7 @@ change_type: enhancement # The name of the component, or a single word describing the area of concern, (e.g. collector, target allocator, auto-instrumentation, opamp, github action) -component: operator +component: instrumentation, collector # A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). note: "Add a must gather utility to help troubleshoot" @@ -13,4 +13,13 @@ issues: [3149] # (Optional) One or more lines of additional information to render under the primary note. # These lines will be padded with 2 spaces and then inserted directly into the document. # Use pipe (|) for multiline entries. -subtext: "The new utility is available as part of a new container image" +subtext: | + The new utility is available as part of a new container image. + + To use the image in a running OpenShift cluster, you need to run the following command: + + ```sh + oc adm must-gather --image=ghcr.io/open-telemetry/opentelemetry-operator/must-gather -- /usr/bin/must-gather --operator-namespace opentelemetry-operator-system + ``` + + See the [README](https://github.com/open-telemetry/opentelemetry-operator/blob/main/cmd/gather/README.md) for more details. diff --git a/.github/workflows/publish-must-gather.yaml b/.github/workflows/publish-must-gather.yaml index 5168ffb129..e1d507d24c 100644 --- a/.github/workflows/publish-must-gather.yaml +++ b/.github/workflows/publish-must-gather.yaml @@ -1,4 +1,4 @@ -name: "Publish Python Auto-Instrumentation" +name: "Publish must-gather image" on: push: @@ -24,11 +24,6 @@ jobs: - name: Unshallow run: git fetch --prune --unshallow - - name: Set env vars for the job - run: | - echo "VERSION_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_ENV - echo "VERSION=$(git describe --tags | sed 's/^v//')" >> $GITHUB_ENV - - name: Build the binary for each supported architecture run: | for platform in $(echo $PLATFORMS | tr "," "\n"); do @@ -42,7 +37,6 @@ jobs: uses: docker/metadata-action@v5 with: images: | - otel/opentelemetry-operator-must-gather ghcr.io/open-telemetry/opentelemetry-operator/must-gather tags: | type=semver,pattern={{version}} diff --git a/cmd/gather/README.md b/cmd/gather/README.md index 84b7c04eca..0599ef4c12 100644 --- a/cmd/gather/README.md +++ b/cmd/gather/README.md @@ -19,10 +19,12 @@ To run the must-gather tool for the OpenTelemetry Operator, use one of the follo ### Using the image from the Operator deployment +This is the recommended way to do it if you are not using OpenShift. + If you want to use the image in a running cluster, you need to run the following command: ```sh -oc adm must-gather --image= -- /usr/bin/must-gather --operator-namespace opentelemetry-operator-system +oc adm must-gather --image=ghcr.io/open-telemetry/opentelemetry-operator/must-gather -- /usr/bin/must-gather --operator-namespace opentelemetry-operator-system ``` ### Using it as a CLI @@ -30,7 +32,5 @@ oc adm must-gather --image= -- /usr/bin/must-gather --operato You only need to build and run: ```sh make must-gather -./bin/must-gather --help +./bin/must-gather_$(go env GOARCH) --help ``` - -This is the recommended way to do it if you are not using OpenShift. \ No newline at end of file From 215ee9c14c64ae81e634848dd76ab33aa98ffb09 Mon Sep 17 00:00:00 2001 From: Israel Blancas Date: Mon, 7 Oct 2024 12:51:04 +0200 Subject: [PATCH 14/14] Fix typo Signed-off-by: Israel Blancas --- .chloggen/3149-add-must-gather.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.chloggen/3149-add-must-gather.yaml b/.chloggen/3149-add-must-gather.yaml index 12affefa8c..d42c553265 100755 --- a/.chloggen/3149-add-must-gather.yaml +++ b/.chloggen/3149-add-must-gather.yaml @@ -2,7 +2,7 @@ change_type: enhancement # The name of the component, or a single word describing the area of concern, (e.g. collector, target allocator, auto-instrumentation, opamp, github action) -component: instrumentation, collector +component: auto-instrumentation, collector # A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). note: "Add a must gather utility to help troubleshoot"