Files
troubleshoot/pkg/loader/loader.go
Evans Mungai 401dfe2c57 feat: add loader APIs to load specs from raw troubleshoot spec (#1202)
* feat: add loader APIs to load specs from a list of yaml docs

The change introduces a loader package that will contain loader
public APIs. The aim of these APIs will be to, given any source of
troubleshoot specs, the loaders will fetch the specs and parse out
all troubleshoot objects that can be extracted.

* Some refactoring

* Some more changes

* More changes caught when testing vendor portal

* Add tests and rename Troubleshoot kinds struct

* Additional test

* Handle ConfigMap and Secrets with multiple specs in them

* Fix failing test

* Revert multidoc split implementation

* Fix merge conflict

* Change LoadFromXXX functions to a single LoadSpecs function
2023-06-06 16:48:29 -04:00

291 lines
9.1 KiB
Go

package loader
import (
"context"
"strings"
"github.com/pkg/errors"
"github.com/replicatedhq/troubleshoot/internal/util"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
"github.com/replicatedhq/troubleshoot/pkg/client/troubleshootclientset/scheme"
"github.com/replicatedhq/troubleshoot/pkg/constants"
"github.com/replicatedhq/troubleshoot/pkg/docrewrite"
"github.com/replicatedhq/troubleshoot/pkg/types"
"gopkg.in/yaml.v2"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/klog/v2"
)
var decoder runtime.Decoder
func init() {
// Allow serializing Secrets and ConfigMaps
_ = v1.AddToScheme(scheme.Scheme)
decoder = scheme.Codecs.UniversalDeserializer()
}
type parsedDoc struct {
Kind string `json:"kind" yaml:"kind"`
APIVersion string `json:"apiVersion" yaml:"apiVersion"`
Data map[string]any `json:"data" yaml:"data"`
StringData map[string]string `json:"stringData" yaml:"stringData"`
}
type LoadOptions struct {
RawSpecs []string
RawSpec string
}
// LoadSpecs takes sources to load specs from and returns a TroubleshootKinds object
// that contains all the parsed troubleshoot specs.
//
// The fetched specs need to be yaml documents. The documents can be a multidoc yaml
// separated by "---" which get split and parsed one at a time. This function will
// return an error if any of the documents are not valid yaml. If Secrets or ConfigMaps
// are found, they will be parsed and the support bundle, redactor or preflight spec
// will be extracted from them, else they will be ignored.
// Any other yaml documents will be ignored.
func LoadSpecs(ctx context.Context, opt LoadOptions) (*TroubleshootKinds, error) {
opt.RawSpecs = append(opt.RawSpecs, opt.RawSpec)
return loadFromStrings(opt.RawSpecs...)
}
type TroubleshootKinds struct {
AnalyzersV1Beta2 []troubleshootv1beta2.Analyzer
CollectorsV1Beta2 []troubleshootv1beta2.Collector
HostCollectorsV1Beta2 []troubleshootv1beta2.HostCollector
HostPreflightsV1Beta2 []troubleshootv1beta2.HostPreflight
PreflightsV1Beta2 []troubleshootv1beta2.Preflight
RedactorsV1Beta2 []troubleshootv1beta2.Redactor
RemoteCollectorsV1Beta2 []troubleshootv1beta2.RemoteCollector
SupportBundlesV1Beta2 []troubleshootv1beta2.SupportBundle
}
func (kinds *TroubleshootKinds) IsEmpty() bool {
return len(kinds.AnalyzersV1Beta2) == 0 &&
len(kinds.CollectorsV1Beta2) == 0 &&
len(kinds.HostCollectorsV1Beta2) == 0 &&
len(kinds.HostPreflightsV1Beta2) == 0 &&
len(kinds.PreflightsV1Beta2) == 0 &&
len(kinds.RedactorsV1Beta2) == 0 &&
len(kinds.RemoteCollectorsV1Beta2) == 0 &&
len(kinds.SupportBundlesV1Beta2) == 0
}
func NewTroubleshootKinds() *TroubleshootKinds {
return &TroubleshootKinds{}
}
// loadFromStrings accepts a list of strings (exploded) which should be yaml documents
func loadFromStrings(rawSpecs ...string) (*TroubleshootKinds, error) {
splitdocs := []string{}
multiRawDocs := []string{}
// 1. First split multidoc yaml documents.
for _, rawSpec := range rawSpecs {
multiRawDocs = append(multiRawDocs, strings.Split(rawSpec, "\n---\n")...)
}
// 2. Go through each document to see if it is a configmap, secret or troubleshoot kind
// For secrets and configmaps, extract support bundle, redactor or preflight specs
// For troubleshoot kinds, pass them through
for _, rawDoc := range multiRawDocs {
var parsed parsedDoc
err := yaml.Unmarshal([]byte(rawDoc), &parsed)
if err != nil {
return nil, types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, errors.Wrapf(err, "failed to parse yaml: '%s'", string(rawDoc)))
}
if isConfigMap(parsed) || isSecret(parsed) {
// Extract specs from configmap or secret
obj, _, err := decoder.Decode([]byte(rawDoc), nil, nil)
if err != nil {
return nil, types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES,
errors.Wrapf(err, "failed to decode raw spec: '%s'", string(rawDoc)),
)
}
// 3. Extract the raw troubleshoot specs
switch v := obj.(type) {
case *v1.ConfigMap:
specs, err := getSpecFromConfigMap(v)
if err != nil {
return nil, types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, err)
}
splitdocs = append(splitdocs, specs...)
case *v1.Secret:
specs, err := getSpecFromSecret(v)
if err != nil {
return nil, types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, err)
}
splitdocs = append(splitdocs, specs...)
default:
return nil, types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, errors.Errorf("%T type is not a Secret or ConfigMap", v))
}
} else if parsed.APIVersion == constants.Troubleshootv1beta2Kind {
// If it's not a configmap or secret, just append it to the splitdocs
splitdocs = append(splitdocs, rawDoc)
} else {
klog.V(1).Infof("skip loading %q kind", parsed.Kind)
}
}
// 4. Then load the specs into the kinds struct
return loadFromSplitDocs(splitdocs)
}
func loadFromSplitDocs(splitdocs []string) (*TroubleshootKinds, error) {
kinds := NewTroubleshootKinds()
for _, doc := range splitdocs {
converted, err := docrewrite.ConvertToV1Beta2([]byte(doc))
if err != nil {
return nil, types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, errors.Wrapf(err, "failed to convert doc to troubleshoot.sh/v1beta2 kind: '%s'", doc))
}
obj, _, err := decoder.Decode([]byte(converted), nil, nil)
if err != nil {
return nil, types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, errors.Wrapf(err, "failed to decode '%s'", converted))
}
switch spec := obj.(type) {
case *troubleshootv1beta2.Analyzer:
kinds.AnalyzersV1Beta2 = append(kinds.AnalyzersV1Beta2, *spec)
case *troubleshootv1beta2.Collector:
kinds.CollectorsV1Beta2 = append(kinds.CollectorsV1Beta2, *spec)
case *troubleshootv1beta2.HostCollector:
kinds.HostCollectorsV1Beta2 = append(kinds.HostCollectorsV1Beta2, *spec)
case *troubleshootv1beta2.HostPreflight:
kinds.HostPreflightsV1Beta2 = append(kinds.HostPreflightsV1Beta2, *spec)
case *troubleshootv1beta2.Preflight:
kinds.PreflightsV1Beta2 = append(kinds.PreflightsV1Beta2, *spec)
case *troubleshootv1beta2.Redactor:
kinds.RedactorsV1Beta2 = append(kinds.RedactorsV1Beta2, *spec)
case *troubleshootv1beta2.RemoteCollector:
kinds.RemoteCollectorsV1Beta2 = append(kinds.RemoteCollectorsV1Beta2, *spec)
case *troubleshootv1beta2.SupportBundle:
kinds.SupportBundlesV1Beta2 = append(kinds.SupportBundlesV1Beta2, *spec)
default:
return nil, types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, errors.Errorf("unknown troubleshoot kind %T", obj))
}
}
klog.V(1).Info("loaded troubleshoot specs successfully")
return kinds, nil
}
func isSecret(parsedDocHead parsedDoc) bool {
if parsedDocHead.Kind == "Secret" && parsedDocHead.APIVersion == "v1" {
return true
}
return false
}
func isConfigMap(parsedDocHead parsedDoc) bool {
if parsedDocHead.Kind == "ConfigMap" && parsedDocHead.APIVersion == "v1" {
return true
}
return false
}
// getSpecFromConfigMap extracts multiple troubleshoot specs from a secret
func getSpecFromConfigMap(cm *v1.ConfigMap) ([]string, error) {
specs := []string{}
str, ok := cm.Data[constants.SupportBundleKey]
if ok {
spec, err := validateYaml(str)
if err != nil {
return nil, err
}
specs = append(specs, util.SplitYAML(spec)...)
}
str, ok = cm.Data[constants.RedactorKey]
if ok {
spec, err := validateYaml(str)
if err != nil {
return nil, err
}
specs = append(specs, util.SplitYAML(spec)...)
}
str, ok = cm.Data[constants.PreflightKey]
if ok {
spec, err := validateYaml(str)
if err != nil {
return nil, err
}
specs = append(specs, util.SplitYAML(spec)...)
}
return specs, nil
}
// getSpecFromSecret extracts multiple troubleshoot specs from a secret
func getSpecFromSecret(secret *v1.Secret) ([]string, error) {
specs := []string{}
specBytes, ok := secret.Data[constants.SupportBundleKey]
if ok {
spec, err := validateYaml(string(specBytes))
if err != nil {
return nil, err
}
specs = append(specs, util.SplitYAML(spec)...)
}
specBytes, ok = secret.Data[constants.RedactorKey]
if ok {
spec, err := validateYaml(string(specBytes))
if err != nil {
return nil, err
}
specs = append(specs, util.SplitYAML(spec)...)
}
specBytes, ok = secret.Data[constants.PreflightKey]
if ok {
spec, err := validateYaml(string(specBytes))
if err != nil {
return nil, err
}
specs = append(specs, util.SplitYAML(spec)...)
}
str, ok := secret.StringData[constants.SupportBundleKey]
if ok {
spec, err := validateYaml(str)
if err != nil {
return nil, err
}
specs = append(specs, util.SplitYAML(spec)...)
}
str, ok = secret.StringData[constants.RedactorKey]
if ok {
spec, err := validateYaml(str)
if err != nil {
return nil, err
}
specs = append(specs, util.SplitYAML(spec)...)
}
str, ok = secret.StringData[constants.PreflightKey]
if ok {
spec, err := validateYaml(str)
if err != nil {
return nil, err
}
specs = append(specs, util.SplitYAML(spec)...)
}
return specs, nil
}
func validateYaml(raw string) (string, error) {
var parsed map[string]any
err := yaml.Unmarshal([]byte(raw), &parsed)
if err != nil {
return "", errors.Wrapf(err, "failed to parse yaml: '%s'", string(raw))
}
return raw, nil
}