diff --git a/pkg/analyze/analyzer.go b/pkg/analyze/analyzer.go index 758475047..20add774c 100644 --- a/pkg/analyze/analyzer.go +++ b/pkg/analyze/analyzer.go @@ -37,6 +37,12 @@ func Analyze(analyzer *troubleshootv1beta1.Analyze, getFile getCollectedFileCont if analyzer.ImagePullSecret != nil { return analyzeImagePullSecret(analyzer.ImagePullSecret, findFiles) } + if analyzer.DeploymentStatus != nil { + return deploymentStatus(analyzer.DeploymentStatus, getFile) + } + if analyzer.StatefulsetStatus != nil { + return statefulsetStatus(analyzer.StatefulsetStatus, getFile) + } return nil, errors.New("invalid analyzer") } diff --git a/pkg/analyze/common_status.go b/pkg/analyze/common_status.go new file mode 100644 index 000000000..a4cb52955 --- /dev/null +++ b/pkg/analyze/common_status.go @@ -0,0 +1,122 @@ +package analyzer + +import ( + "strconv" + "strings" + + "github.com/pkg/errors" + troubleshootv1beta1 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta1" +) + +func commonStatus(outcomes []*troubleshootv1beta1.Outcome, title string, readyReplicas int) (*AnalyzeResult, error) { + result := &AnalyzeResult{ + Title: title, + } + + // ordering from the spec is important, the first one that matches returns + for _, outcome := range outcomes { + if outcome.Fail != nil { + if outcome.Fail.When == "" { + result.IsFail = true + result.Message = outcome.Fail.Message + result.URI = outcome.Fail.URI + + return result, nil + } + + match, err := compareActualToWhen(outcome.Fail.When, readyReplicas) + if err != nil { + return nil, errors.Wrap(err, "failed to parse fail range") + } + + if match { + result.IsFail = true + result.Message = outcome.Fail.Message + result.URI = outcome.Fail.URI + + return result, nil + } + } else if outcome.Warn != nil { + if outcome.Warn.When == "" { + result.IsWarn = true + result.Message = outcome.Warn.Message + result.URI = outcome.Warn.URI + + return result, nil + } + + match, err := compareActualToWhen(outcome.Warn.When, readyReplicas) + if err != nil { + return nil, errors.Wrap(err, "failed to parse warn range") + } + + if match { + result.IsWarn = true + result.Message = outcome.Warn.Message + result.URI = outcome.Warn.URI + + return result, nil + } + } else if outcome.Pass != nil { + if outcome.Pass.When == "" { + result.IsPass = true + result.Message = outcome.Pass.Message + result.URI = outcome.Pass.URI + + return result, nil + } + + match, err := compareActualToWhen(outcome.Pass.When, readyReplicas) + if err != nil { + return nil, errors.Wrap(err, "failed to parse pass range") + } + + if match { + result.IsPass = true + result.Message = outcome.Pass.Message + result.URI = outcome.Pass.URI + + return result, nil + } + } + } + + return result, nil +} + +func compareActualToWhen(when string, actual int) (bool, error) { + parts := strings.Split(strings.TrimSpace(when), " ") + + // we can make this a lot more flexible + if len(parts) != 2 { + return false, errors.New("unable to parse when range") + } + + value, err := strconv.Atoi(parts[1]) + if err != nil { + return false, errors.New("unable to parse when value") + } + + switch parts[0] { + case "=": + fallthrough + case "==": + fallthrough + case "===": + return actual == value, nil + + case "<": + return actual < value, nil + + case ">": + return actual > value, nil + + case "<=": + return actual <= value, nil + + case ">=": + return actual >= value, nil + } + + return false, errors.Errorf("unknown comparator: %q", parts[0]) +} diff --git a/pkg/analyze/data_test.go b/pkg/analyze/data_test.go new file mode 100644 index 000000000..b04644219 --- /dev/null +++ b/pkg/analyze/data_test.go @@ -0,0 +1,454 @@ +package analyzer + +var collectedDeployments = `[ + { + "metadata": { + "name": "kotsadm-api", + "namespace": "default", + "selfLink": "/apis/apps/v1/namespaces/default/deployments/kotsadm-api", + "uid": "56526035-cd29-4d08-8375-291503b1a006", + "resourceVersion": "1583068", + "generation": 1, + "creationTimestamp": "2019-11-07T00:34:32Z", + "labels": { + "app.kubernetes.io/managed-by": "skaffold-v0.41.0" + }, + "annotations": { + "deployment.kubernetes.io/revision": "1", + "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"apps/v1\",\"kind\":\"Deployment\",\"metadata\":{\"annotations\":{},\"labels\":{\"app.kubernetes.io/managed-by\":\"skaffold-v0.41.0\",\"skaffold.dev/builder\":\"local\",\"skaffold.dev/cleanup\":\"true\",\"skaffold.dev/deployer\":\"kustomize\",\"skaffold.dev/docker-api-version\":\"1.40\",\"skaffold.dev/run-id\":\"98f0a02b-9739-4d94-ba11-3e4d273c743e\",\"skaffold.dev/tag-policy\":\"git-commit\",\"skaffold.dev/tail\":\"true\"},\"name\":\"kotsadm-api\",\"namespace\":\"default\"},\"spec\":{\"selector\":{\"matchLabels\":{\"app\":\"kotsadm-api\"}},\"template\":{\"metadata\":{\"labels\":{\"app\":\"kotsadm-api\",\"app.kubernetes.io/managed-by\":\"skaffold-v0.41.0\",\"skaffold.dev/builder\":\"local\",\"skaffold.dev/cleanup\":\"true\",\"skaffold.dev/deployer\":\"kustomize\",\"skaffold.dev/docker-api-version\":\"1.40\",\"skaffold.dev/run-id\":\"98f0a02b-9739-4d94-ba11-3e4d273c743e\",\"skaffold.dev/tag-policy\":\"git-commit\",\"skaffold.dev/tail\":\"true\"}},\"spec\":{\"affinity\":{\"podAffinity\":{\"preferredDuringSchedulingIgnoredDuringExecution\":[{\"podAffinityTerm\":{\"labelSelector\":{\"matchExpressions\":[{\"key\":\"app\",\"operator\":\"In\",\"values\":[\"ship-www\"]}]},\"topologyKey\":\"kubernetes.io/hostname\"},\"weight\":1}]},\"podAntiAffinity\":{\"preferredDuringSchedulingIgnoredDuringExecution\":[{\"podAffinityTerm\":{\"labelSelector\":{\"matchExpressions\":[{\"key\":\"app\",\"operator\":\"In\",\"values\":[\"kotsadm-api\"]}]},\"topologyKey\":\"kubernetes.io/hostname\"},\"weight\":2}]}},\"containers\":[{\"env\":[{\"name\":\"DEV_NAMESPACE\",\"value\":\"test\"},{\"name\":\"LOG_LEVEL\",\"value\":\"debug\"},{\"name\":\"SESSION_KEY\",\"valueFrom\":{\"secretKeyRef\":{\"key\":\"key\",\"name\":\"session\"}}},{\"name\":\"POSTGRES_URI\",\"valueFrom\":{\"secretKeyRef\":{\"key\":\"uri\",\"name\":\"ship-postgres\"}}},{\"name\":\"API_ENCRYPTION_KEY\",\"value\":\"IvWItkB8+ezMisPjSMBknT1PdKjBx7Xc/txZqOP8Y2Oe7+Jy\"},{\"name\":\"INIT_SERVER_URI\",\"value\":\"http://init-server:3000\"},{\"name\":\"WATCH_SERVER_URI\",\"value\":\"http://watch-server:3000\"},{\"name\":\"PINO_LOG_PRETTY\",\"value\":\"1\"},{\"name\":\"S3_BUCKET_NAME\",\"value\":\"shipbucket\"},{\"name\":\"AIRGAP_BUNDLE_S3_BUCKET\",\"value\":\"airgap\"},{\"name\":\"S3_ENDPOINT\",\"value\":\"http://kotsadm-s3.default.svc.cluster.local:4569/\"},{\"name\":\"S3_ACCESS_KEY_ID\",\"value\":\"***HIDDEN***\"},{\"name\":\"S3_SECRET_ACCESS_KEY\",\"value\":\"***HIDDEN***\"},{\"name\":\"S3_BUCKET_ENDPOINT\",\"value\":\"true\"},{\"name\":\"GITHUB_CLIENT_ID\",\"valueFrom\":{\"secretKeyRef\":{\"key\":\"client-id\",\"name\":\"github-app\"}}},{\"name\":\"GITHUB_CLIENT_SECRET\",\"valueFrom\":{\"secretKeyRef\":{\"key\":\"client-secret\",\"name\":\"github-app\"}}},{\"name\":\"GITHUB_INTEGRATION_ID\",\"valueFrom\":{\"secretKeyRef\":{\"key\":\"integration-id\",\"name\":\"github-app\"}}},{\"name\":\"GITHUB_PRIVATE_KEY_FILE\",\"value\":\"/keys/github/private-key.pem\"},{\"name\":\"SHIP_API_ENDPOINT\",\"value\":\"http://kotsadm-api.default.svc.cluster.local:3000\"},{\"name\":\"SHIP_API_ADVERTISE_ENDPOINT\",\"value\":\"http://localhost:30065\"},{\"name\":\"GRAPHQL_PREM_ENDPOINT\",\"value\":\"http://graphql-api-prem:3000/graphql\"},{\"name\":\"AUTO_CREATE_CLUSTER\",\"value\":\"1\"},{\"name\":\"AUTO_CREATE_CLUSTER_NAME\",\"value\":\"microk8s\"},{\"name\":\"AUTO_CREATE_CLUSTER_TOKEN\",\"value\":\"***HIDDEN***\"},{\"name\":\"ENABLE_SHIP\",\"value\":\"1\"},{\"name\":\"ENABLE_KOTS\",\"value\":\"1\"},{\"name\":\"ENABLE_KURL\",\"value\":\"1\"},{\"name\":\"POD_NAMESPACE\",\"valueFrom\":{\"fieldRef\":{\"fieldPath\":\"metadata.namespace\"}}}],\"image\":\"localhost:32000/kotsadm-api:v1.0.1-30-g8fa13e34-dirty@sha256:4a0ca1a2eae46472bd2d454f9e763a458ddd172689e179b17054262507bb4fc8\",\"imagePullPolicy\":\"IfNotPresent\",\"name\":\"kotsadm-api\",\"ports\":[{\"containerPort\":3000,\"name\":\"http\"},{\"containerPort\":9229,\"name\":\"debug\"}],\"readinessProbe\":{\"httpGet\":{\"path\":\"/healthz\",\"port\":3000},\"initialDelaySeconds\":2,\"periodSeconds\":2},\"volumeMounts\":[{\"mountPath\":\"/keys/github\",\"name\":\"github-app-private-key\",\"readOnly\":true}]}],\"restartPolicy\":\"Always\",\"securityContext\":{\"runAsUser\":0},\"serviceAccount\":\"kotsadm-api\",\"volumes\":[{\"name\":\"github-app-private-key\",\"secret\":{\"secretName\":\"github-app-private-key\"}}]}}}}\n" + } + }, + "spec": { + "replicas": 1, + "selector": { + "matchLabels": { + "app": "kotsadm-api" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "kotsadm-api" + } + }, + "spec": { + "containers": [ + { + "name": "kotsadm-api", + "image": "localhost:32000/kotsadm-api:v1.0.1-30-g8fa13e34-dirty@sha256:4a0ca1a2eae46472bd2d454f9e763a458ddd172689e179b17054262507bb4fc8", + "ports": [ + { + "name": "http", + "containerPort": 3000, + "protocol": "TCP" + } + ], + "env": [ + { + "name": "DEV_NAMESPACE", + "value": "test" + }, + { + "name": "POD_NAMESPACE", + "valueFrom": { + "fieldRef": { + "apiVersion": "v1", + "fieldPath": "metadata.namespace" + } + } + } + ], + "resources": {}, + "readinessProbe": { + "httpGet": { + "path": "/healthz", + "port": 3000, + "scheme": "HTTP" + }, + "initialDelaySeconds": 2, + "timeoutSeconds": 1, + "periodSeconds": 2, + "successThreshold": 1, + "failureThreshold": 3 + }, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File", + "imagePullPolicy": "IfNotPresent" + } + ], + "restartPolicy": "Always", + "terminationGracePeriodSeconds": 30, + "dnsPolicy": "ClusterFirst", + "serviceAccountName": "kotsadm-api", + "serviceAccount": "kotsadm-api", + "schedulerName": "default-scheduler" + } + }, + "strategy": { + "type": "RollingUpdate", + "rollingUpdate": { + "maxUnavailable": "25%", + "maxSurge": "25%" + } + }, + "revisionHistoryLimit": 10, + "progressDeadlineSeconds": 600 + }, + "status": { + "observedGeneration": 1, + "replicas": 1, + "updatedReplicas": 1, + "readyReplicas": 1, + "availableReplicas": 1, + "conditions": [ + { + "type": "Available", + "status": "True", + "lastUpdateTime": "2019-11-07T00:35:23Z", + "lastTransitionTime": "2019-11-07T00:35:23Z", + "reason": "MinimumReplicasAvailable", + "message": "Deployment has minimum availability." + }, + { + "type": "Progressing", + "status": "True", + "lastUpdateTime": "2019-11-07T00:35:23Z", + "lastTransitionTime": "2019-11-07T00:34:32Z", + "reason": "NewReplicaSetAvailable", + "message": "ReplicaSet \"kotsadm-api-6f4b994bd5\" has successfully progressed." + } + ] + } + }, + { + "metadata": { + "name": "kotsadm-operator", + "namespace": "default", + "selfLink": "/apis/apps/v1/namespaces/default/deployments/kotsadm-operator", + "uid": "cfae9877-eef4-44c9-acac-0bf0d1aa547e", + "resourceVersion": "1583379", + "generation": 2, + "creationTimestamp": "2019-11-07T00:34:32Z", + "labels": { + "app.kubernetes.io/managed-by": "skaffold-v0.41.0" + }, + "annotations": { + "deployment.kubernetes.io/revision": "2", + "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"apps/v1\",\"kind\":\"Deployment\",\"metadata\":{\"annotations\":{},\"labels\":{\"app.kubernetes.io/managed-by\":\"skaffold-v0.41.0\",\"skaffold.dev/builder\":\"local\",\"skaffold.dev/cleanup\":\"true\",\"skaffold.dev/deployer\":\"kustomize\",\"skaffold.dev/docker-api-version\":\"1.40\",\"skaffold.dev/run-id\":\"98f0a02b-9739-4d94-ba11-3e4d273c743e\",\"skaffold.dev/tag-policy\":\"git-commit\",\"skaffold.dev/tail\":\"true\"},\"name\":\"kotsadm-operator\",\"namespace\":\"default\"},\"spec\":{\"selector\":{\"matchLabels\":{\"app\":\"kotsadm-operator\"}},\"template\":{\"metadata\":{\"labels\":{\"app\":\"kotsadm-operator\",\"app.kubernetes.io/managed-by\":\"skaffold-v0.41.0\",\"skaffold.dev/builder\":\"local\",\"skaffold.dev/cleanup\":\"true\",\"skaffold.dev/deployer\":\"kustomize\",\"skaffold.dev/docker-api-version\":\"1.40\",\"skaffold.dev/run-id\":\"98f0a02b-9739-4d94-ba11-3e4d273c743e\",\"skaffold.dev/tag-policy\":\"git-commit\",\"skaffold.dev/tail\":\"true\"}},\"spec\":{\"containers\":[{\"env\":[{\"name\":\"KOTSADM_API_ENDPOINT\",\"value\":\"http://kotsadm-api:3000\"},{\"name\":\"KOTSADM_TOKEN\",\"value\":\"***HIDDEN***\"},{\"name\":\"KOTSADM_TARGET_NAMESPACE\",\"value\":\"test\"}],\"image\":\"localhost:32000/kotsadm-operator:v1.0.1-30-g8fa13e34-dirty@sha256:177c15b6399717048e9355bc8fd8b8ed213be90615c7c6ee7b7fdcee50aca6c5\",\"imagePullPolicy\":\"Always\",\"name\":\"kotsadm-operator\",\"resources\":{\"limits\":{\"cpu\":\"200m\",\"memory\":\"1000Mi\"},\"requests\":{\"cpu\":\"100m\",\"memory\":\"500Mi\"}}}],\"restartPolicy\":\"Always\"}}}}\n" + } + }, + "spec": { + "replicas": 1, + "selector": { + "matchLabels": { + "app": "kotsadm-operator" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "kotsadm-operator" + } + }, + "spec": { + "containers": [ + { + "name": "kotsadm-operator", + "image": "localhost:32000/kotsadm-operator:v1.0.1-30-g8fa13e34-dirty@sha256:177c15b6399717048e9355bc8fd8b8ed213be90615c7c6ee7b7fdcee50aca6c5", + "env": [ + { + "name": "KOTSADM_API_ENDPOINT", + "value": "http://kotsadm-api:3000" + }, + { + "name": "KOTSADM_TOKEN", + "value": "***HIDDEN***" + }, + { + "name": "KOTSADM_TARGET_NAMESPACE", + "value": "test" + } + ], + "resources": { + "limits": { + "cpu": "200m", + "memory": "1000Mi" + }, + "requests": { + "cpu": "100m", + "memory": "500Mi" + } + }, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File", + "imagePullPolicy": "Always" + } + ], + "restartPolicy": "Always", + "terminationGracePeriodSeconds": 30, + "dnsPolicy": "ClusterFirst", + "securityContext": {}, + "schedulerName": "default-scheduler" + } + }, + "strategy": { + "type": "RollingUpdate", + "rollingUpdate": { + "maxUnavailable": "25%", + "maxSurge": "25%" + } + }, + "revisionHistoryLimit": 10, + "progressDeadlineSeconds": 600 + }, + "status": { + "observedGeneration": 2, + "replicas": 1, + "updatedReplicas": 1, + "readyReplicas": 1, + "availableReplicas": 1, + "conditions": [ + { + "type": "Available", + "status": "True", + "lastUpdateTime": "2019-11-07T00:34:37Z", + "lastTransitionTime": "2019-11-07T00:34:37Z", + "reason": "MinimumReplicasAvailable", + "message": "Deployment has minimum availability." + }, + { + "type": "Progressing", + "status": "True", + "lastUpdateTime": "2019-11-07T00:36:34Z", + "lastTransitionTime": "2019-11-07T00:34:32Z", + "reason": "NewReplicaSetAvailable", + "message": "ReplicaSet \"kotsadm-operator-5b5c977699\" has successfully progressed." + } + ] + } + }, + { + "metadata": { + "name": "kotsadm-postgres-watch", + "namespace": "default", + "selfLink": "/apis/apps/v1/namespaces/default/deployments/kotsadm-postgres-watch", + "uid": "ec195b07-4fd2-4bbe-8b01-4cbdfbe0e79d", + "resourceVersion": "1582762", + "generation": 1, + "creationTimestamp": "2019-11-07T00:34:39Z", + "annotations": { + "deployment.kubernetes.io/revision": "1" + }, + "ownerReferences": [ + { + "apiVersion": "databases.schemahero.io/v1alpha2", + "kind": "Database", + "name": "kotsadm-postgres", + "uid": "e6d51ce6-c4c1-428f-9e8e-56f1a3a43588", + "controller": true, + "blockOwnerDeletion": true + } + ] + }, + "spec": { + "replicas": 1, + "selector": { + "matchLabels": { + "deployment": "kotsadm-postgreswatch" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "deployment": "kotsadm-postgreswatch" + } + }, + "spec": { + "containers": [ + { + "name": "schemahero", + "image": "schemahero/schemahero:alpha", + "args": [ + "watch", + "--driver", + "postgres", + "--uri", + "postgres://shipcloud:password@postgres.default.svc.cluster.local:5432/shipcloud?sslmode=disable", + "--namespace", + "default", + "--instance", + "kotsadm-postgres" + ], + "resources": {}, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File", + "imagePullPolicy": "Always" + } + ], + "restartPolicy": "Always", + "terminationGracePeriodSeconds": 30, + "dnsPolicy": "ClusterFirst", + "serviceAccountName": "kotsadm-postgres", + "serviceAccount": "kotsadm-postgres", + "securityContext": {}, + "schedulerName": "default-scheduler" + } + }, + "strategy": { + "type": "RollingUpdate", + "rollingUpdate": { + "maxUnavailable": "25%", + "maxSurge": "25%" + } + }, + "revisionHistoryLimit": 10, + "progressDeadlineSeconds": 600 + }, + "status": { + "observedGeneration": 1, + "replicas": 1, + "updatedReplicas": 1, + "readyReplicas": 1, + "availableReplicas": 1, + "conditions": [ + { + "type": "Available", + "status": "True", + "lastUpdateTime": "2019-11-07T00:35:00Z", + "lastTransitionTime": "2019-11-07T00:35:00Z", + "reason": "MinimumReplicasAvailable", + "message": "Deployment has minimum availability." + }, + { + "type": "Progressing", + "status": "True", + "lastUpdateTime": "2019-11-07T00:35:00Z", + "lastTransitionTime": "2019-11-07T00:34:39Z", + "reason": "NewReplicaSetAvailable", + "message": "ReplicaSet \"kotsadm-postgres-watch-5cf76f4c45\" has successfully progressed." + } + ] + } + }, + { + "metadata": { + "name": "kotsadm-web", + "namespace": "default", + "selfLink": "/apis/apps/v1/namespaces/default/deployments/kotsadm-web", + "uid": "4e657f8a-5edb-498b-9402-1f93f51f5dda", + "resourceVersion": "1582354", + "generation": 1, + "creationTimestamp": "2019-11-07T00:34:32Z", + "labels": { + "app.kubernetes.io/managed-by": "skaffold-v0.41.0", + "skaffold.dev/builder": "local", + "skaffold.dev/cleanup": "true", + "skaffold.dev/deployer": "kustomize", + "skaffold.dev/docker-api-version": "1.40", + "skaffold.dev/run-id": "98f0a02b-9739-4d94-ba11-3e4d273c743e", + "skaffold.dev/tag-policy": "git-commit", + "skaffold.dev/tail": "true" + }, + "annotations": { + "deployment.kubernetes.io/revision": "1", + "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"apps/v1\",\"kind\":\"Deployment\",\"metadata\":{\"annotations\":{},\"labels\":{\"app.kubernetes.io/managed-by\":\"skaffold-v0.41.0\",\"skaffold.dev/builder\":\"local\",\"skaffold.dev/cleanup\":\"true\",\"skaffold.dev/deployer\":\"kustomize\",\"skaffold.dev/docker-api-version\":\"1.40\",\"skaffold.dev/run-id\":\"98f0a02b-9739-4d94-ba11-3e4d273c743e\",\"skaffold.dev/tag-policy\":\"git-commit\",\"skaffold.dev/tail\":\"true\"},\"name\":\"kotsadm-web\",\"namespace\":\"default\"},\"spec\":{\"selector\":{\"matchLabels\":{\"app\":\"kotsadm-web\"}},\"template\":{\"metadata\":{\"labels\":{\"app\":\"kotsadm-web\",\"app.kubernetes.io/managed-by\":\"skaffold-v0.41.0\",\"skaffold.dev/builder\":\"local\",\"skaffold.dev/cleanup\":\"true\",\"skaffold.dev/deployer\":\"kustomize\",\"skaffold.dev/docker-api-version\":\"1.40\",\"skaffold.dev/run-id\":\"98f0a02b-9739-4d94-ba11-3e4d273c743e\",\"skaffold.dev/tag-policy\":\"git-commit\",\"skaffold.dev/tail\":\"true\"}},\"spec\":{\"containers\":[{\"env\":[{\"name\":\"GITHUB_CLIENT_ID\",\"valueFrom\":{\"secretKeyRef\":{\"key\":\"client-id\",\"name\":\"github-app\"}}},{\"name\":\"GITHUB_INSTALL_URL\",\"valueFrom\":{\"secretKeyRef\":{\"key\":\"install-url\",\"name\":\"github-app\"}}},{\"name\":\"SHIP_CLUSTER_API_SERVER\",\"value\":\"http://localhost:30065\"},{\"name\":\"SHIP_CLUSTER_WEB_URI\",\"value\":\"http://localhost:8000\"}],\"image\":\"localhost:32000/kotsadm-web:v1.0.1-30-g8fa13e34@sha256:5b5b5b640b6e09d8b3185d4ae15ac4dc558d4e2ea034ac3e567d8cce04eadb9c\",\"imagePullPolicy\":\"IfNotPresent\",\"name\":\"kotsadm-web\",\"ports\":[{\"containerPort\":8000,\"name\":\"http\"}]}]}}}}\n" + } + }, + "spec": { + "replicas": 1, + "selector": { + "matchLabels": { + "app": "kotsadm-web" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "kotsadm-web", + "app.kubernetes.io/managed-by": "skaffold-v0.41.0", + "skaffold.dev/builder": "local", + "skaffold.dev/cleanup": "true", + "skaffold.dev/deployer": "kustomize", + "skaffold.dev/docker-api-version": "1.40", + "skaffold.dev/run-id": "98f0a02b-9739-4d94-ba11-3e4d273c743e", + "skaffold.dev/tag-policy": "git-commit", + "skaffold.dev/tail": "true" + } + }, + "spec": { + "containers": [ + { + "name": "kotsadm-web", + "image": "localhost:32000/kotsadm-web:v1.0.1-30-g8fa13e34@sha256:5b5b5b640b6e09d8b3185d4ae15ac4dc558d4e2ea034ac3e567d8cce04eadb9c", + "ports": [ + { + "name": "http", + "containerPort": 8000, + "protocol": "TCP" + } + ], + "env": [ + { + "name": "GITHUB_CLIENT_ID", + "valueFrom": { + "secretKeyRef": { + "name": "github-app", + "key": "client-id" + } + } + } + ], + "resources": {}, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File", + "imagePullPolicy": "IfNotPresent" + } + ], + "restartPolicy": "Always", + "terminationGracePeriodSeconds": 30, + "dnsPolicy": "ClusterFirst", + "securityContext": {}, + "schedulerName": "default-scheduler" + } + }, + "strategy": { + "type": "RollingUpdate", + "rollingUpdate": { + "maxUnavailable": "25%", + "maxSurge": "25%" + } + }, + "revisionHistoryLimit": 10, + "progressDeadlineSeconds": 600 + }, + "status": { + "observedGeneration": 1, + "replicas": 1, + "updatedReplicas": 1, + "readyReplicas": 1, + "availableReplicas": 1, + "conditions": [ + { + "type": "Available", + "status": "True", + "lastUpdateTime": "2019-11-07T00:34:39Z", + "lastTransitionTime": "2019-11-07T00:34:39Z", + "reason": "MinimumReplicasAvailable", + "message": "Deployment has minimum availability." + }, + { + "type": "Progressing", + "status": "True", + "lastUpdateTime": "2019-11-07T00:34:39Z", + "lastTransitionTime": "2019-11-07T00:34:32Z", + "reason": "NewReplicaSetAvailable", + "message": "ReplicaSet \"kotsadm-web-79bfb95c48\" has successfully progressed." + } + ] + } + } + ]` diff --git a/pkg/analyze/deployment_status.go b/pkg/analyze/deployment_status.go new file mode 100644 index 000000000..0314cc021 --- /dev/null +++ b/pkg/analyze/deployment_status.go @@ -0,0 +1,41 @@ +package analyzer + +import ( + "encoding/json" + "fmt" + "path" + + "github.com/pkg/errors" + troubleshootv1beta1 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta1" + appsv1 "k8s.io/api/apps/v1" +) + +func deploymentStatus(analyzer *troubleshootv1beta1.DeploymentStatus, getCollectedFileContents func(string) ([]byte, error)) (*AnalyzeResult, error) { + collected, err := getCollectedFileContents(path.Join("cluster-resources", "deployments", fmt.Sprintf("%s.json", analyzer.Namespace))) + if err != nil { + return nil, errors.Wrap(err, "failed to read collected deployments from namespace") + } + + var deployments []appsv1.Deployment + if err := json.Unmarshal(collected, &deployments); err != nil { + return nil, errors.Wrap(err, "failed to unmarshal deployment list") + } + + var status *appsv1.DeploymentStatus + for _, deployment := range deployments { + if deployment.Name == analyzer.Name { + status = &deployment.Status + } + } + + if status == nil { + // there's not an error, but maybe the requested deployment is not even deployed + return &AnalyzeResult{ + Title: fmt.Sprintf("%s Deployment Status", analyzer.Name), + IsFail: true, + Message: "not found", + }, nil + } + + return commonStatus(analyzer.Outcomes, fmt.Sprintf("%s Status", analyzer.Name), int(status.ReadyReplicas)) +} diff --git a/pkg/analyze/deployment_status_test.go b/pkg/analyze/deployment_status_test.go new file mode 100644 index 000000000..01878c8ca --- /dev/null +++ b/pkg/analyze/deployment_status_test.go @@ -0,0 +1,131 @@ +package analyzer + +import ( + "testing" + + troubleshootv1beta1 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta1" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_deploymentStatus(t *testing.T) { + tests := []struct { + name string + analyzer troubleshootv1beta1.DeploymentStatus + expectResult AnalyzeResult + files map[string][]byte + }{ + { + name: "1/1, pass when = 1", + analyzer: troubleshootv1beta1.DeploymentStatus{ + Outcomes: []*troubleshootv1beta1.Outcome{ + { + Pass: &troubleshootv1beta1.SingleOutcome{ + When: "= 1", + Message: "pass", + }, + }, + { + Fail: &troubleshootv1beta1.SingleOutcome{ + Message: "fail", + }, + }, + }, + Namespace: "default", + Name: "kotsadm-api", + }, + expectResult: AnalyzeResult{ + IsPass: true, + IsWarn: false, + IsFail: false, + Title: "", + Message: "pass", + }, + files: map[string][]byte{ + "cluster-resources/deployments/default.json": []byte(collectedDeployments), + }, + }, + { + name: "1/1, pass when = 2", + analyzer: troubleshootv1beta1.DeploymentStatus{ + Outcomes: []*troubleshootv1beta1.Outcome{ + { + Pass: &troubleshootv1beta1.SingleOutcome{ + When: "= 2", + Message: "pass", + }, + }, + { + Fail: &troubleshootv1beta1.SingleOutcome{ + Message: "fail", + }, + }, + }, + Namespace: "default", + Name: "kotsadm-api", + }, + expectResult: AnalyzeResult{ + IsPass: false, + IsWarn: false, + IsFail: true, + Title: "", + Message: "fail", + }, + files: map[string][]byte{ + "cluster-resources/deployments/default.json": []byte(collectedDeployments), + }, + }, + { + name: "1/1, pass when >= 2, warn when = 1, fail when 0", + analyzer: troubleshootv1beta1.DeploymentStatus{ + Outcomes: []*troubleshootv1beta1.Outcome{ + { + Pass: &troubleshootv1beta1.SingleOutcome{ + When: ">= 2", + Message: "pass", + }, + }, + { + Warn: &troubleshootv1beta1.SingleOutcome{ + When: "= 1", + Message: "warn", + }, + }, + { + Fail: &troubleshootv1beta1.SingleOutcome{ + Message: "fail", + }, + }, + }, + Namespace: "default", + Name: "kotsadm-api", + }, + expectResult: AnalyzeResult{ + IsPass: false, + IsWarn: true, + IsFail: false, + Title: "", + Message: "warn", + }, + files: map[string][]byte{ + "cluster-resources/deployments/default.json": []byte(collectedDeployments), + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + req := require.New(t) + + getFiles := func(n string) ([]byte, error) { + return test.files[n], nil + } + + actual, err := deploymentStatus(&test.analyzer, getFiles) + req.NoError(err) + + assert.Equal(t, &test.expectResult, actual) + + }) + } +} diff --git a/pkg/analyze/statefulset_status.go b/pkg/analyze/statefulset_status.go new file mode 100644 index 000000000..0c7b419cb --- /dev/null +++ b/pkg/analyze/statefulset_status.go @@ -0,0 +1,41 @@ +package analyzer + +import ( + "encoding/json" + "fmt" + "path" + + "github.com/pkg/errors" + troubleshootv1beta1 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta1" + appsv1 "k8s.io/api/apps/v1" +) + +func statefulsetStatus(analyzer *troubleshootv1beta1.StatefulsetStatus, getCollectedFileContents func(string) ([]byte, error)) (*AnalyzeResult, error) { + collected, err := getCollectedFileContents(path.Join("cluster-resources", "statefulsets", fmt.Sprintf("%s.json", analyzer.Namespace))) + if err != nil { + return nil, errors.Wrap(err, "failed to read collected deployments from namespace") + } + + var statefulsets []appsv1.StatefulSet + if err := json.Unmarshal(collected, &statefulsets); err != nil { + return nil, errors.Wrap(err, "failed to unmarshal statefulset list") + } + + var status *appsv1.StatefulSetStatus + for _, statefulset := range statefulsets { + if statefulset.Name == analyzer.Name { + status = &statefulset.Status + } + } + + if status == nil { + // there's not an error, but maybe the requested statefulset is not even deployed + return &AnalyzeResult{ + Title: fmt.Sprintf("%s Statefulset Status", analyzer.Name), + IsFail: true, + Message: "not found", + }, nil + } + + return commonStatus(analyzer.Outcomes, fmt.Sprintf("%s Status", analyzer.Name), int(status.ReadyReplicas)) +} diff --git a/pkg/apis/troubleshoot/v1beta1/analyzer_shared.go b/pkg/apis/troubleshoot/v1beta1/analyzer_shared.go index fbfe99897..c9d28d3a1 100644 --- a/pkg/apis/troubleshoot/v1beta1/analyzer_shared.go +++ b/pkg/apis/troubleshoot/v1beta1/analyzer_shared.go @@ -50,6 +50,20 @@ type ImagePullSecret struct { RegistryName string `json:"registryName" yaml:"registryName"` } +type DeploymentStatus struct { + AnalyzeMeta `json:",inline" yaml:",inline"` + Outcomes []*Outcome `json:"outcomes" yaml:"outcomes"` + Namespace string `json:"namespace" yaml:"namespace"` + Name string `json:"name" yaml:"name"` +} + +type StatefulsetStatus struct { + AnalyzeMeta `json:",inline" yaml:",inline"` + Outcomes []*Outcome `json:"outcomes" yaml:"outcomes"` + Namespace string `json:"namespace" yaml:"namespace"` + Name string `json:"name" yaml:"name"` +} + type AnalyzeMeta struct { CheckName string `json:"checkName,omitempty" yaml:"checkName,omitempty"` } @@ -61,4 +75,6 @@ type Analyze struct { Ingress *Ingress `json:"ingress,omitempty" yaml:"ingress,omitempty"` Secret *AnalyzeSecret `json:"secret,omitempty" yaml:"secret,omitempty"` ImagePullSecret *ImagePullSecret `json:"imagePullSecret,omitempty" yaml:"imagePullSecret,omitempty"` + DeploymentStatus *DeploymentStatus `json:"deploymentStatus,omitempty" yaml:"deploymentStatus,omitempty"` + StatefulsetStatus *StatefulsetStatus `json:"statefulsetStatus,omitempty" yaml:"statefulsetStatus,omitempty"` } diff --git a/pkg/apis/troubleshoot/v1beta1/zz_generated.deepcopy.go b/pkg/apis/troubleshoot/v1beta1/zz_generated.deepcopy.go index 618926aa5..537f10415 100644 --- a/pkg/apis/troubleshoot/v1beta1/zz_generated.deepcopy.go +++ b/pkg/apis/troubleshoot/v1beta1/zz_generated.deepcopy.go @@ -82,6 +82,16 @@ func (in *Analyze) DeepCopyInto(out *Analyze) { *out = new(ImagePullSecret) (*in).DeepCopyInto(*out) } + if in.DeploymentStatus != nil { + in, out := &in.DeploymentStatus, &out.DeploymentStatus + *out = new(DeploymentStatus) + (*in).DeepCopyInto(*out) + } + if in.StatefulsetStatus != nil { + in, out := &in.StatefulsetStatus, &out.StatefulsetStatus + *out = new(StatefulsetStatus) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Analyze. @@ -731,6 +741,33 @@ func (in *CustomResourceDefinition) DeepCopy() *CustomResourceDefinition { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DeploymentStatus) DeepCopyInto(out *DeploymentStatus) { + *out = *in + out.AnalyzeMeta = in.AnalyzeMeta + if in.Outcomes != nil { + in, out := &in.Outcomes, &out.Outcomes + *out = make([]*Outcome, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(Outcome) + (*in).DeepCopyInto(*out) + } + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeploymentStatus. +func (in *DeploymentStatus) DeepCopy() *DeploymentStatus { + if in == nil { + return nil + } + out := new(DeploymentStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Exec) DeepCopyInto(out *Exec) { *out = *in @@ -1307,6 +1344,33 @@ func (in *SingleOutcome) DeepCopy() *SingleOutcome { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *StatefulsetStatus) DeepCopyInto(out *StatefulsetStatus) { + *out = *in + out.AnalyzeMeta = in.AnalyzeMeta + if in.Outcomes != nil { + in, out := &in.Outcomes, &out.Outcomes + *out = make([]*Outcome, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(Outcome) + (*in).DeepCopyInto(*out) + } + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StatefulsetStatus. +func (in *StatefulsetStatus) DeepCopy() *StatefulsetStatus { + if in == nil { + return nil + } + out := new(StatefulsetStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *StorageClass) DeepCopyInto(out *StorageClass) { *out = *in