diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..dea6f56 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,27 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Additional context** +Add any other context about the problem here. \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..24473de --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/task.md b/.github/ISSUE_TEMPLATE/task.md new file mode 100644 index 0000000..dd5ad80 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/task.md @@ -0,0 +1,19 @@ +--- +name: Task 🔧 +about: Internal things, technical debt, and to-do tasks to be performed. +title: '' +labels: 'kind/task' +assignees: '' + +--- +### Is your task related to a problem? Please describe. + + +### Describe the solution you'd like + + +### Describe alternatives you've considered + + +### Additional context + \ No newline at end of file diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..1931f14 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,29 @@ +**What type of PR is this?** +> Uncomment only one ` /kind` line, and delete the rest. +> For example, `> /kind bug` would simply become: `/kind bug` + +> /kind bug +> /kind cleanup +> /kind failing-test +> /kind enhancement +> /kind documentation +> /kind code-refactoring + + +**What does this PR do / why we need it**: + +**Have you updated the necessary documentation?** + +* [ ] Documentation update is required by this PR. +* [ ] Documentation has been updated. + +**Which issue(s) this PR fixes**: + +Fixes #? + +**Test acceptance criteria**: + +* [ ] Unit Test +* [ ] E2E Test + +**How to test changes / Special notes to the reviewer**: \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..4529260 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,16 @@ +FROM quay.io/openshift/origin-must-gather:4.6 + +# Save original gather script +RUN mv /usr/bin/gather /usr/bin/gather_original + +# Use our gather script in place of the original one +COPY gather_gitops.sh /usr/bin/gather + +# Make it executable +RUN chmod +x /usr/bin/gather + +# install jq to parse json within bash scripts +RUN curl -o /usr/local/bin/jq http://stedolan.github.io/jq/download/linux64/jq && \ + chmod +x /usr/local/bin/jq + +ENTRYPOINT /usr/bin/gather \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..74d4e8d --- /dev/null +++ b/Makefile @@ -0,0 +1,54 @@ +## Copyright 2019 Red Hat, Inc. +## +## 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. + +default: build + +HUB ?= quay.io/redhat-developer +TAG ?= latest + +lint: + @if command -v shellcheck >/dev/null; then \ + find . -name '*.sh' -print0 | xargs -0 -r shellcheck; \ + else \ + echo "shellcheck not found, installing it now..."; \ + if command -v apt-get >/dev/null; then \ + sudo apt-get install -y shellcheck; \ + elif command -v yum >/dev/null; then \ + sudo yum install -y shellcheck; \ + elif command -v dnf >/dev/null; then \ + sudo dnf install -y shellcheck; \ + elif command -v pacman >/dev/null; then \ + sudo pacman -S --noconfirm shellcheck; \ + else \ + echo "shellcheck not found and unable to install it automatically"; \ + echo "Please install shellcheck manually and run the lint target again"; \ + exit 1; \ + fi; \ + find . -name '*.sh' -print0 | xargs -0 -r shellcheck; \ + fi + +image: + @if command -v podman >/dev/null; then \ + podman build -t ${HUB}/gitops-must-gather:${TAG} .; \ + else \ + docker build -t ${HUB}/gitops-must-gather:${TAG} .; \ + fi + +push: image + podman push ${HUB}/gitops-must-gather:${TAG} + @if command -v podman >/dev/null; then \ + podman push ${HUB}/gitops-must-gather:${TAG} .; \ + else \ + docker push ${HUB}/gitops-must-gather:${TAG} .; \ + fi \ No newline at end of file diff --git a/README.md b/README.md index 2911dcc..6683cfa 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,86 @@ -# gitops-must-gather -A client tool for gathering GitOps Operator information in a OpenShift cluster. +# GitOps Operator Must-Gather +================= + +`GitOps must-gather` is a tool to gather information about the gitop-operator. It is built on top of [OpenShift must-gather](https://github.com/openshift/must-gather). + +### Usage +```sh +oc adm must-gather --image=quay.io/redhat-developer/gitops-must-gather:latest +``` + +The command above will create a local directory with a dump of the OpenShift GitOps state. Note that this command will only get data related to the GitOps Operator in your OpenShift cluster. + +You will get a dump of: +- Information for the subscription of the gitops-operator +- The GitOps Operator namespace (and its children objects) +- All namespaces where ArgoCD objects exist in, plus all objects in those namespaces, such as ArgoCD, Applications, ApplicationSets, and AppProjects, and configmaps + - No secrets will be collected +- A list of list of the namespaces that are managed by gitops-operator identified namespaces and resources from those namespaces. +- All GitOps CRD's objects and definitions +- Operator logs +- Logs of Argo CD +- Warning and error-level Events + +In order to get data about other parts of the cluster (not specific to gitops-operator) you should run just `oc adm must-gather` (without passing a custom image). Run `oc adm must-gather -h` to see more options. + +An example of the GitOps must-gather output would be something like the following, where there are two argocd instances in namespaces `openshift-gitops` and `foo` and an additional namespace called `foo-managed` which is managed by namespace `foo`: +``` +cluster-gitops + └── gitops + ├── appprojects.yaml + ├── crds.yaml + ├── namespace_openshift-gitops_resources + │   ├── application_controller_logs.txt + │   ├── applications + │   ├── applicationsets + │   ├── argocd.yaml + │   ├── deployments + │   │   ├── cluster.yaml + │   │   └── kam.yaml + │   ├── dex-server_logs.txt + │   ├── error-events.txt + │   ├── pods + │   │   ├── cluster-5db4b95547-rdz2m.yaml + │   │   └── kam-fff7f474f-d27c8.yaml + │   ├── redis_logs.txt + │   ├── replicasets + │   │   ├── cluster-5db4b95547.yaml + │   │   └── kam-fff7f474f.yaml + │   ├── repo-server_logs.txt + │   ├── routes + │   │   └── kam.yaml + │   ├── server_logs.txt + │   ├── services + │   │   ├── cluster.yaml + │   │   └── kam.yaml + │   ├── statefulsets + │   └── warning-events.txt + ├── namespace_foo_resources + │   ├── application_controller_logs.txt + │   ├── applications + │   │   └── guestbook.yaml + │   ├── applicationsets + │   │   └── guestbook.yaml + │   ├── argocd.yaml + │   ├── deployments + │   ├── dex-server_logs.txt + │   ├── error-events.txt + │   ├── managedNamespace_foo-managed + │   │   ├── deployments + │   │   ├── pods + │   │   ├── replicasets + │   │   ├── routes + │   │   ├── services + │   │   └── statefulsets + │   ├── pods + │   ├── redis_logs.txt + │   ├── replicasets + │   ├── repo-server_logs.txt + │   ├── routes + │   ├── server_logs.txt + │   ├── services + │   ├── statefulsets + │   └── warning-events.txt + ├── oc-version.txt + └── subscription.yaml +``` \ No newline at end of file diff --git a/gather_gitops.sh b/gather_gitops.sh new file mode 100644 index 0000000..b6a6e0e --- /dev/null +++ b/gather_gitops.sh @@ -0,0 +1,186 @@ +#!/bin/bash +BASE_COLLECTION_PATH="/must-gather" + +GITOPS_COLLECTION_PATH="$BASE_COLLECTION_PATH/cluster-gitops" +GITOPS_DIR="$GITOPS_COLLECTION_PATH/gitops" + +# Checks if a binary is present on the local system +exit_if_binary_not_installed() { + for binary in "$@"; do + command -v "$binary" >/dev/null 2>&1 || { + echo >&2 "Script requires '$binary' command-line utility to be installed on your local machine. Aborting..." + exit 1 + } + done +} + +function getNamespaces() { + local namespaces + local default="openshift-gitops" + local clusterScopedInstances + clusterScopedInstances=$(oc get subs openshift-gitops-operator -n openshift-operators -o json | jq '.spec.config.env[]?|select(.name=="ARGOCD_CLUSTER_CONFIG_NAMESPACES").value' | tr -d '",') + if [[ "$(oc get subs openshift-gitops-operator -n openshift-operators -o jsonpath='{.spec.config.env}')" == "" ]]; then + namespaces=${default} + elif [[ "${clusterScopedInstances}" != "" ]]; then + if [[ "$(oc get subs openshift-gitops-operator -n openshift-operators -o json | jq '.spec.config.env[]?|select(.name=="DISABLE_DEFAULT_ARGOCD_INSTANCE").value')" != "false" ]]; then + namespaces+="${clusterScopedInstances} ${default}" + else + namespaces="${clusterScopedInstances}" + fi + else + echo "No gitops instances found, please check your cluster configuration." > "${GITOPS_DIR}"/must-gather-script-errors.yaml 2>&1 + fi + + local argocdInstances + argocdInstances=$(oc get ArgoCD --all-namespaces -o jsonpath='{.items[*].metadata.namespace}') + + local total + total="${namespaces} ${argocdInstances}" + echo "${total}" + + NAMESPACES=$(echo "${total}" | tr ' ' '\n' | sort -u | tr '\n' ' ') + export NAMESPACES +} + +function main() { + + exit_if_binary_not_installed "kubectl" "oc" + getNamespaces + + echo "Starting GitOps Operator must-gather script..." + mkdir -p "$GITOPS_DIR" + + echo "getting OpenShift Cluster Version..." + oc version > "${GITOPS_DIR}"/oc-version.txt 2>&1 + + echo "getting GitOps Operator Subscription..." + oc get subs openshift-gitops-operator -n openshift-operators > "${GITOPS_DIR}"/subscription.yaml 2>&1 + + for namespace in ${NAMESPACES}; do + mkdir -p "${GITOPS_DIR}"/namespace_"${namespace}"_resources + + echo "getting pods in ${namespace}..." + mkdir -p "${GITOPS_DIR}"/namespace_"${namespace}"_resources/pods + for pod in $(oc get pods -n "${namespace}" -o jsonpath='{ .items[*].metadata.name }') ; do + oc -n "${namespace}" get pod/"${pod}" -o yaml > "${GITOPS_DIR}"/namespace_"${namespace}"_resources/pods/"${pod}".yaml 2>&1 + done + + echo "getting deployments in ${namespace}..." + mkdir -p "${GITOPS_DIR}"/namespace_"${namespace}"_resources/deployments + for deployment in $(oc get deployments -n "${namespace}" -o jsonpath='{ .items[*].metadata.name }') ; do + oc -n "${namespace}" get deployment/"${deployment}" -o yaml > "${GITOPS_DIR}"/namespace_"${namespace}"_resources/deployments/"${deployment}".yaml 2>&1 + done + + echo "getting services in ${namespace}..." + mkdir -p "${GITOPS_DIR}"/namespace_"${namespace}"_resources/services + for service in $(oc get services -n "${namespace}" -o jsonpath='{ .items[*].metadata.name }') ; do + oc -n "${namespace}" get service/"${service}" -o yaml > "${GITOPS_DIR}"/namespace_"${namespace}"_resources/services/"${service}".yaml 2>&1 + done + + echo "getting replicaSets in ${namespace}..." + mkdir -p "${GITOPS_DIR}"/namespace_"${namespace}"_resources/replicasets + for replicaset in $(oc get replicasets -n "${namespace}" -o jsonpath='{ .items[*].metadata.name }') ; do + oc -n "${namespace}" get replicaset/"${replicaset}" -o yaml > "${GITOPS_DIR}"/namespace_"${namespace}"_resources/replicasets/"${replicaset}".yaml 2>&1 + done + + echo "getting statefulsets in ${namespace}..." + mkdir -p "${GITOPS_DIR}"/namespace_"${namespace}"_resources/statefulsets + for statefulset in $(oc get statefulsets -n "${namespace}" -o jsonpath='{ .items[*].metadata.name }') ; do + oc -n "${namespace}" get statefulset/"${statefulset}" -o yaml > "${GITOPS_DIR}"/namespace_"${namespace}"_resources/statefulsets/"${statefulset}".yaml 2>&1 + done + + echo "getting routes in ${namespace}..." + mkdir -p "${GITOPS_DIR}"/namespace_"${namespace}"_resources/routes + for route in $(oc get routes -n "${namespace}" -o jsonpath='{ .items[*].metadata.name }') ; do + oc -n "${namespace}" get route/"${route}" -o yaml > "${GITOPS_DIR}"/namespace_"${namespace}"_resources/routes/"${route}".yaml 2>&1 + done + + echo "getting ArgoCD in ${namespace}..." + oc -n "${namespace}" get argocd -o yaml > "${GITOPS_DIR}"/namespace_"${namespace}"_resources/argocd.yaml 2>&1 + + echo "getting Applications in ${namespace}..." + mkdir -p "${GITOPS_DIR}"/namespace_"${namespace}"_resources/applications + for application in $(oc get applications.argoproj.io -n "${namespace}" -o jsonpath='{ .items[*].metadata.name }') ; do + oc -n "${namespace}" get applications.argoproj.io/"${application}" -o yaml > "${GITOPS_DIR}"/namespace_"${namespace}"_resources/applications/"${application}".yaml 2>&1 + done + + local sourceNamespaces + sourceNamespaces=$(oc get argocd -n "${namespace}" -o jsonpath='{.items[*].spec.sourceNamespaces[*]}' ) + if [[ "${sourceNamespaces}" != "" ]] ; then + for sourceNamespace in ${sourceNamespaces} ; do + local sourceNamespaceApps + sourceNamespaceApps=$(oc get applications.argoproj.io -n "${sourceNamespace}" -o jsonpath='{ .items[*].metadata.name }' ) + for sourceNamespaceApp in ${sourceNamespaceApps}; do + oc -n "${sourceNamespace}" get applications.argoproj.io/"${sourceNamespaceApp}" -o yaml > "${GITOPS_DIR}"/namespace_"${namespace}"_resources/applications/"${sourceNamespaceApp}"_sourceNamespace_"${sourceNamespace}".yaml 2>&1 + done + done + fi + + echo "getting ApplicationSets in ${namespace}..." + mkdir -p "${GITOPS_DIR}"/namespace_"${namespace}"_resources/applicationsets + for applicationset in $(oc get applicationset.argoproj.io -n "${namespace}" -o jsonpath='{ .items[*].metadata.name }') ; do + oc -n "${namespace}" get applicationset.argoproj.io/"${applicationset}" -o yaml > "${GITOPS_DIR}"/namespace_"${namespace}"_resources/applicationsets/"${applicationset}".yaml 2>&1 + done + + echo "getting warning events in ${namespace}..." + oc get events -n openshift-gitops --field-selector type=Warning > "${GITOPS_DIR}"/namespace_"${namespace}"_resources/warning-events.txt 2>&1 + + echo "getting error events in ${namespace}..." + oc get events -n openshift-gitops --field-selector type=Error > "${GITOPS_DIR}"/namespace_"${namespace}"_resources/error-events.txt 2>&1 + + # getting logs + local argoCDName + argoCDName=$(oc -n "${namespace}" get argocd -o jsonpath='{.items[*].metadata.name}') + oc logs statefulset/"${argoCDName}"-application-controller -n "${namespace}" > "${GITOPS_DIR}"/namespace_"${namespace}"_resources/application_controller_logs.txt 2>&1 + oc logs deployment/"${argoCDName}"-server -n "${namespace}" > "${GITOPS_DIR}"/namespace_"${namespace}"_resources/server_logs.txt 2>&1 + oc logs deployment/"${argoCDName}"-repo-server -n "${namespace}" > "${GITOPS_DIR}"/namespace_"${namespace}"_resources/repo-server_logs.txt 2>&1 + oc logs deployment/oc logs deployment/"${argoCDName}"-redis -n "${namespace}" > "${GITOPS_DIR}"/namespace_"${namespace}"_resources/redis_logs.txt 2>&1 + oc logs deployment/"${argoCDName}"-dex-server -n "${namespace}" > "${GITOPS_DIR}"/namespace_"${namespace}"_resources/dex-server_logs.txt 2>&1 + + local managedNamespaces + managedNamespaces=$(oc get namespaces --selector=argocd.argoproj.io/managed-by="${namespace}" -o jsonpath='{.items[*].metadata.name}') + + for managedNamespace in ${managedNamespaces}; do + + mkdir -p "${GITOPS_DIR}"/namespace_"${namespace}"_resources/managedNamespace_"${managedNamespace}"/pods + for pod in $(oc get pods -n "${managedNamespace}" -o jsonpath='{ .items[*].metadata.name }') ; do + oc -n "${managedNamespace}" get pod/"${pod}" -o yaml > "${GITOPS_DIR}"/namespace_"${namespace}"_resources/managedNamespace_"${managedNamespace}"/pod_"${pod}".yaml + done + + mkdir -p "${GITOPS_DIR}"/namespace_"${namespace}"_resources/managedNamespace_"${managedNamespace}"/deployments + for deployment in $(oc get deployments -n "${managedNamespace}" -o jsonpath='{ .items[*].metadata.name }') ; do + oc -n "${managedNamespace}" get deployment/"${deployment}" -o yaml > "${GITOPS_DIR}"/namespace_"${namespace}"_resources/managedNamespace_"${managedNamespace}"/deployment/"${deployment}".yaml + done + + mkdir -p "${GITOPS_DIR}"/namespace_"${namespace}"_resources/managedNamespace_"${managedNamespace}"/services + for service in $(oc get services -n "${managedNamespace}" -o jsonpath='{ .items[*].metadata.name }') ; do + oc -n "${managedNamespace}" get service/"${service}" -o yaml > "${GITOPS_DIR}"/namespace_"${namespace}"_resources/managedNamespace_"${managedNamespace}"/services/"${service}".yaml + done + + mkdir -p "${GITOPS_DIR}"/namespace_"${namespace}"_resources/managedNamespace_"${managedNamespace}"/replicasets + for replicaset in $(oc get replicasets -n "${managedNamespace}" -o jsonpath='{ .items[*].metadata.name }') ; do + oc -n "${namespace}" get replicaset/"${replicaset}" -o yaml > "${GITOPS_DIR}"/namespace_"${namespace}"_resources/managedNamespace_"${managedNamespace}"/replicasets/"${replicaset}".yaml + done + + mkdir -p "${GITOPS_DIR}"/namespace_"${namespace}"_resources/managedNamespace_"${managedNamespace}"/statefulsets + for statefulset in $(oc get statefulsets -n "${managedNamespace}" -o jsonpath='{ .items[*].metadata.name }') ; do + oc -n "${managedNamespace}" get statefulset/"${statefulset}" -o yaml > "${GITOPS_DIR}"/namespace_"${namespace}"_resources/managedNamespace_"${managedNamespace}"/statefulsets/"${statefulset}".yaml + done + + mkdir -p "${GITOPS_DIR}"/namespace_"${namespace}"_resources/managedNamespace_"${managedNamespace}"/routes + for route in $(oc get routes -n "${namespace}" -o jsonpath='{ .items[*].metadata.name }') ; do + oc -n "${managedNamespace}" get route/"${route}" -o yaml > "${GITOPS_DIR}"/namespace_"${namespace}"_resources/managedNamespace_"${managedNamespace}"/routes/"${route}".yaml + done + done + done + + echo "getting AppProjects..." + oc get appProjects.argoproj.io --all-namespaces -o yaml > "${GITOPS_DIR}"/appprojects.yaml 2>&1 + + echo "getting GitOps CRDs..." + oc get crds -l operators.coreos.com/openshift-gitops-operator.openshift-operators > "${GITOPS_DIR}"/crds.yaml 2>&1 + + echo "Done! Thank you for using the GitOps must-gather tool :)" +} + +main "$@"