Skip to content

Commit

Permalink
Search stdin for secrets with preflight specs (#1153)
Browse files Browse the repository at this point in the history
* we can now read preflight specs out of secrets, either from stdin or file input

* moved spec read logic out into its own function so it can be unit
tested easier

* added more comprehensive unit testing on the different ways we can read in specs
  • Loading branch information
Nathan Sullivan authored May 16, 2023
1 parent 95c83f0 commit 6de79af
Show file tree
Hide file tree
Showing 5 changed files with 572 additions and 141 deletions.
197 changes: 197 additions & 0 deletions pkg/preflight/read_specs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
package preflight

import (
"encoding/base64"
"fmt"
"io"
"net/http"
"net/url"
"os"
"strings"

"github.com/pkg/errors"
"github.com/replicatedhq/troubleshoot/cmd/util"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
troubleshootclientsetscheme "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/oci"
"github.com/replicatedhq/troubleshoot/pkg/specs"
"github.com/replicatedhq/troubleshoot/pkg/types"
"gopkg.in/yaml.v2"
"k8s.io/client-go/kubernetes/scheme"
)

type documentHead struct {
Kind string `yaml:"kind"`
Metadata documentMetadata `yaml:"metadata,omitempty"`
Data documentData `yaml:"data,omitempty"`
StringData documentStringData `yaml:"stringData,omitempty"`
}

type documentMetadata struct {
Labels documentMetadataLabels `yaml:"labels,omitempty"`
}

type documentMetadataLabels struct {
TroubleshootKind string `yaml:"troubleshoot.io/kind,omitempty"`
}

type documentData struct {
PreflightYaml string `yaml:"preflight.yaml,omitempty"`
}

type documentStringData struct {
PreflightYaml string `yaml:"preflight.yaml,omitempty"`
}

type PreflightSpecs struct {
PreflightSpec *troubleshootv1beta2.Preflight
HostPreflightSpec *troubleshootv1beta2.HostPreflight
UploadResultSpecs []*troubleshootv1beta2.Preflight
}

func (p *PreflightSpecs) Read(args []string) error {
var preflightContent []byte
var err error

for _, v := range args {
if strings.HasPrefix(v, "secret/") {
// format secret/namespace-name/secret-name
pathParts := strings.Split(v, "/")
if len(pathParts) != 3 {
return types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, errors.Errorf("path %s must have 3 components", v))
}

spec, err := specs.LoadFromSecret(pathParts[1], pathParts[2], "preflight-spec")
if err != nil {
return types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, errors.Wrap(err, "failed to get spec from secret"))
}

preflightContent = spec
} else if _, err = os.Stat(v); err == nil {
b, err := os.ReadFile(v)
if err != nil {
return types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, err)
}

preflightContent = b
} else if v == "-" {
b, err := io.ReadAll(os.Stdin)
if err != nil {
return types.NewExitCodeError(constants.EXIT_CODE_CATCH_ALL, err)
}
preflightContent = b
} else {
u, err := url.Parse(v)
if err != nil {
return types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, err)
}

if u.Scheme == "oci" {
content, err := oci.PullPreflightFromOCI(v)
if err != nil {
if err == oci.ErrNoRelease {
return types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, errors.Errorf("no release found for %s.\nCheck the oci:// uri for errors or contact the application vendor for support.", v))
}

return types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, err)
}

preflightContent = content
} else {
if !util.IsURL(v) {
return types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, fmt.Errorf("%s is not a URL and was not found (err %s)", v, err))
}

req, err := http.NewRequest("GET", v, nil)
if err != nil {
// exit code: should this be catch all or spec issues...?
return types.NewExitCodeError(constants.EXIT_CODE_CATCH_ALL, err)
}
req.Header.Set("User-Agent", "Replicated_Preflight/v1beta2")
resp, err := http.DefaultClient.Do(req)
if err != nil {
// exit code: should this be catch all or spec issues...?
return types.NewExitCodeError(constants.EXIT_CODE_CATCH_ALL, err)
}
defer resp.Body.Close()

body, err := io.ReadAll(resp.Body)
if err != nil {
return types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, err)
}

preflightContent = body
}
}

multidocs := strings.Split(string(preflightContent), "\n---\n")

for _, doc := range multidocs {
var parsedDocHead documentHead

err := yaml.Unmarshal([]byte(doc), &parsedDocHead)
if err != nil {
return types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, errors.Wrap(err, "failed to parse yaml"))
}

// We're going to look for either of "kind: Preflight" OR "kind: Secret" with the label "troubleshoot.io/kind: preflight"

if parsedDocHead.Kind != "Preflight" && parsedDocHead.Kind != "Secret" {
continue
}

if parsedDocHead.Kind == "Secret" {
if parsedDocHead.Metadata.Labels.TroubleshootKind == "preflight" {
secretSpecDoc := ""
// In a Secret, we need to get the document out of the data.`preflight.yaml` or stringData.`preflight.yaml` (stringData takes precedence)
if len(parsedDocHead.Data.PreflightYaml) > 0 {
b64DecPreflightYaml, err := base64.StdEncoding.DecodeString(parsedDocHead.Data.PreflightYaml)
if err != nil {
return types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, errors.Wrap(err, "failed to base64 decode preflight secret data"))
}
secretSpecDoc = strings.TrimSpace(string(b64DecPreflightYaml))
}
// Do stringData second, if found overwrite data (as per K8s docs)
if len(parsedDocHead.StringData.PreflightYaml) > 0 {
secretSpecDoc = parsedDocHead.StringData.PreflightYaml
}
//
if len(secretSpecDoc) > 0 {
doc = secretSpecDoc
} else {
return types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, errors.Wrap(err, "secret spec with preflight label found, but no preflight stringData or data?"))
}
} else {
// Not a preflight spec, skip
continue
}
}

preflightContent, err = docrewrite.ConvertToV1Beta2([]byte(doc))
if err != nil {
return types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, errors.Wrap(err, "failed to convert to v1beta2"))
}

troubleshootclientsetscheme.AddToScheme(scheme.Scheme)
decode := scheme.Codecs.UniversalDeserializer().Decode
obj, _, err := decode([]byte(preflightContent), nil, nil)
if err != nil {
return types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, errors.Wrapf(err, "failed to parse %s", v))
}

if spec, ok := obj.(*troubleshootv1beta2.Preflight); ok {
if spec.Spec.UploadResultsTo == "" {
p.PreflightSpec = ConcatPreflightSpec(p.PreflightSpec, spec)
} else {
p.UploadResultSpecs = append(p.UploadResultSpecs, spec)
}
} else if spec, ok := obj.(*troubleshootv1beta2.HostPreflight); ok {
p.HostPreflightSpec = ConcatHostPreflightSpec(p.HostPreflightSpec, spec)
}
}
}

return nil
}
Loading

0 comments on commit 6de79af

Please sign in to comment.