Skip to content

Commit 6de79af

Browse files
author
Nathan Sullivan
authored
Search stdin for secrets with preflight specs (#1153)
* 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
1 parent 95c83f0 commit 6de79af

File tree

5 files changed

+572
-141
lines changed

5 files changed

+572
-141
lines changed

pkg/preflight/read_specs.go

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
package preflight
2+
3+
import (
4+
"encoding/base64"
5+
"fmt"
6+
"io"
7+
"net/http"
8+
"net/url"
9+
"os"
10+
"strings"
11+
12+
"github.com/pkg/errors"
13+
"github.com/replicatedhq/troubleshoot/cmd/util"
14+
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
15+
troubleshootclientsetscheme "github.com/replicatedhq/troubleshoot/pkg/client/troubleshootclientset/scheme"
16+
"github.com/replicatedhq/troubleshoot/pkg/constants"
17+
"github.com/replicatedhq/troubleshoot/pkg/docrewrite"
18+
"github.com/replicatedhq/troubleshoot/pkg/oci"
19+
"github.com/replicatedhq/troubleshoot/pkg/specs"
20+
"github.com/replicatedhq/troubleshoot/pkg/types"
21+
"gopkg.in/yaml.v2"
22+
"k8s.io/client-go/kubernetes/scheme"
23+
)
24+
25+
type documentHead struct {
26+
Kind string `yaml:"kind"`
27+
Metadata documentMetadata `yaml:"metadata,omitempty"`
28+
Data documentData `yaml:"data,omitempty"`
29+
StringData documentStringData `yaml:"stringData,omitempty"`
30+
}
31+
32+
type documentMetadata struct {
33+
Labels documentMetadataLabels `yaml:"labels,omitempty"`
34+
}
35+
36+
type documentMetadataLabels struct {
37+
TroubleshootKind string `yaml:"troubleshoot.io/kind,omitempty"`
38+
}
39+
40+
type documentData struct {
41+
PreflightYaml string `yaml:"preflight.yaml,omitempty"`
42+
}
43+
44+
type documentStringData struct {
45+
PreflightYaml string `yaml:"preflight.yaml,omitempty"`
46+
}
47+
48+
type PreflightSpecs struct {
49+
PreflightSpec *troubleshootv1beta2.Preflight
50+
HostPreflightSpec *troubleshootv1beta2.HostPreflight
51+
UploadResultSpecs []*troubleshootv1beta2.Preflight
52+
}
53+
54+
func (p *PreflightSpecs) Read(args []string) error {
55+
var preflightContent []byte
56+
var err error
57+
58+
for _, v := range args {
59+
if strings.HasPrefix(v, "secret/") {
60+
// format secret/namespace-name/secret-name
61+
pathParts := strings.Split(v, "/")
62+
if len(pathParts) != 3 {
63+
return types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, errors.Errorf("path %s must have 3 components", v))
64+
}
65+
66+
spec, err := specs.LoadFromSecret(pathParts[1], pathParts[2], "preflight-spec")
67+
if err != nil {
68+
return types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, errors.Wrap(err, "failed to get spec from secret"))
69+
}
70+
71+
preflightContent = spec
72+
} else if _, err = os.Stat(v); err == nil {
73+
b, err := os.ReadFile(v)
74+
if err != nil {
75+
return types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, err)
76+
}
77+
78+
preflightContent = b
79+
} else if v == "-" {
80+
b, err := io.ReadAll(os.Stdin)
81+
if err != nil {
82+
return types.NewExitCodeError(constants.EXIT_CODE_CATCH_ALL, err)
83+
}
84+
preflightContent = b
85+
} else {
86+
u, err := url.Parse(v)
87+
if err != nil {
88+
return types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, err)
89+
}
90+
91+
if u.Scheme == "oci" {
92+
content, err := oci.PullPreflightFromOCI(v)
93+
if err != nil {
94+
if err == oci.ErrNoRelease {
95+
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))
96+
}
97+
98+
return types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, err)
99+
}
100+
101+
preflightContent = content
102+
} else {
103+
if !util.IsURL(v) {
104+
return types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, fmt.Errorf("%s is not a URL and was not found (err %s)", v, err))
105+
}
106+
107+
req, err := http.NewRequest("GET", v, nil)
108+
if err != nil {
109+
// exit code: should this be catch all or spec issues...?
110+
return types.NewExitCodeError(constants.EXIT_CODE_CATCH_ALL, err)
111+
}
112+
req.Header.Set("User-Agent", "Replicated_Preflight/v1beta2")
113+
resp, err := http.DefaultClient.Do(req)
114+
if err != nil {
115+
// exit code: should this be catch all or spec issues...?
116+
return types.NewExitCodeError(constants.EXIT_CODE_CATCH_ALL, err)
117+
}
118+
defer resp.Body.Close()
119+
120+
body, err := io.ReadAll(resp.Body)
121+
if err != nil {
122+
return types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, err)
123+
}
124+
125+
preflightContent = body
126+
}
127+
}
128+
129+
multidocs := strings.Split(string(preflightContent), "\n---\n")
130+
131+
for _, doc := range multidocs {
132+
var parsedDocHead documentHead
133+
134+
err := yaml.Unmarshal([]byte(doc), &parsedDocHead)
135+
if err != nil {
136+
return types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, errors.Wrap(err, "failed to parse yaml"))
137+
}
138+
139+
// We're going to look for either of "kind: Preflight" OR "kind: Secret" with the label "troubleshoot.io/kind: preflight"
140+
141+
if parsedDocHead.Kind != "Preflight" && parsedDocHead.Kind != "Secret" {
142+
continue
143+
}
144+
145+
if parsedDocHead.Kind == "Secret" {
146+
if parsedDocHead.Metadata.Labels.TroubleshootKind == "preflight" {
147+
secretSpecDoc := ""
148+
// In a Secret, we need to get the document out of the data.`preflight.yaml` or stringData.`preflight.yaml` (stringData takes precedence)
149+
if len(parsedDocHead.Data.PreflightYaml) > 0 {
150+
b64DecPreflightYaml, err := base64.StdEncoding.DecodeString(parsedDocHead.Data.PreflightYaml)
151+
if err != nil {
152+
return types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, errors.Wrap(err, "failed to base64 decode preflight secret data"))
153+
}
154+
secretSpecDoc = strings.TrimSpace(string(b64DecPreflightYaml))
155+
}
156+
// Do stringData second, if found overwrite data (as per K8s docs)
157+
if len(parsedDocHead.StringData.PreflightYaml) > 0 {
158+
secretSpecDoc = parsedDocHead.StringData.PreflightYaml
159+
}
160+
//
161+
if len(secretSpecDoc) > 0 {
162+
doc = secretSpecDoc
163+
} else {
164+
return types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, errors.Wrap(err, "secret spec with preflight label found, but no preflight stringData or data?"))
165+
}
166+
} else {
167+
// Not a preflight spec, skip
168+
continue
169+
}
170+
}
171+
172+
preflightContent, err = docrewrite.ConvertToV1Beta2([]byte(doc))
173+
if err != nil {
174+
return types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, errors.Wrap(err, "failed to convert to v1beta2"))
175+
}
176+
177+
troubleshootclientsetscheme.AddToScheme(scheme.Scheme)
178+
decode := scheme.Codecs.UniversalDeserializer().Decode
179+
obj, _, err := decode([]byte(preflightContent), nil, nil)
180+
if err != nil {
181+
return types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, errors.Wrapf(err, "failed to parse %s", v))
182+
}
183+
184+
if spec, ok := obj.(*troubleshootv1beta2.Preflight); ok {
185+
if spec.Spec.UploadResultsTo == "" {
186+
p.PreflightSpec = ConcatPreflightSpec(p.PreflightSpec, spec)
187+
} else {
188+
p.UploadResultSpecs = append(p.UploadResultSpecs, spec)
189+
}
190+
} else if spec, ok := obj.(*troubleshootv1beta2.HostPreflight); ok {
191+
p.HostPreflightSpec = ConcatHostPreflightSpec(p.HostPreflightSpec, spec)
192+
}
193+
}
194+
}
195+
196+
return nil
197+
}

0 commit comments

Comments
 (0)