-
Notifications
You must be signed in to change notification settings - Fork 35
/
rest.go
220 lines (185 loc) · 5.71 KB
/
rest.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
// Copyright 2023 Stacklok, 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.
// Package rest provides the REST rule data ingest engine
package rest
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strings"
"text/template"
"github.com/google/go-github/v61/github"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"google.golang.org/protobuf/reflect/protoreflect"
engif "github.com/stacklok/minder/internal/engine/interfaces"
"github.com/stacklok/minder/internal/util"
pb "github.com/stacklok/minder/pkg/api/protobuf/go/minder/v1"
provifv1 "github.com/stacklok/minder/pkg/providers/v1"
)
const (
// RestRuleDataIngestType is the type of the REST rule data ingest engine
RestRuleDataIngestType = "rest"
)
type ingestorFallback struct {
// httpCode is the HTTP status code to return
httpCode int
// Body is the body to return
body string
}
// Ingestor is the engine for a rule type that uses REST data ingest
type Ingestor struct {
restCfg *pb.RestType
cli provifv1.REST
endpointTemplate *template.Template
method string
fallback []ingestorFallback
}
// NewRestRuleDataIngest creates a new REST rule data ingest engine
func NewRestRuleDataIngest(
restCfg *pb.RestType,
cli provifv1.REST,
) (*Ingestor, error) {
if len(restCfg.Endpoint) == 0 {
return nil, fmt.Errorf("missing endpoint")
}
tmpl, err := util.ParseNewTextTemplate(&restCfg.Endpoint, "endpoint")
if err != nil {
return nil, fmt.Errorf("cannot parse endpoint template: %w", err)
}
method := util.HttpMethodFromString(restCfg.Method, http.MethodGet)
fallback := make([]ingestorFallback, len(restCfg.Fallback))
for _, fb := range restCfg.Fallback {
fb := fb
fallback = append(fallback, ingestorFallback{
httpCode: int(fb.HttpCode),
body: fb.Body,
})
}
return &Ingestor{
restCfg: restCfg,
cli: cli,
endpointTemplate: tmpl,
method: method,
fallback: fallback,
}, nil
}
// EndpointTemplateParams is the parameters for the REST endpoint template
type EndpointTemplateParams struct {
// Entity is the entity to be evaluated
Entity any
// Params are the parameters to be used in the template
Params map[string]any
}
// GetType returns the type of the REST rule data ingest engine
func (*Ingestor) GetType() string {
return RestRuleDataIngestType
}
// GetConfig returns the config for the REST rule data ingest engine
func (rdi *Ingestor) GetConfig() protoreflect.ProtoMessage {
return rdi.restCfg
}
// Ingest calls the REST endpoint and returns the data
func (rdi *Ingestor) Ingest(ctx context.Context, ent protoreflect.ProtoMessage, params map[string]any) (*engif.Result, error) {
endpoint := new(bytes.Buffer)
retp := &EndpointTemplateParams{
Entity: ent,
Params: params,
}
if err := rdi.endpointTemplate.Execute(endpoint, retp); err != nil {
return nil, fmt.Errorf("cannot execute endpoint template: %w", err)
}
// create string buffer
var bodyr io.Reader
if rdi.restCfg.Body != nil {
bodyr = strings.NewReader(*rdi.restCfg.Body)
}
req, err := rdi.cli.NewRequest(rdi.method, endpoint.String(), bodyr)
if err != nil {
return nil, fmt.Errorf("cannot create request: %w", err)
}
respRdr, err := rdi.doRequest(ctx, req)
if err != nil {
return nil, fmt.Errorf("cannot do request: %w", err)
}
defer func() {
if err := respRdr.Close(); err != nil {
log.Printf("cannot close response body: %v", err)
}
}()
data, err := rdi.parseBody(respRdr)
if err != nil {
return nil, fmt.Errorf("cannot parse body: %w", err)
}
return &engif.Result{
Object: data,
}, nil
}
func (rdi *Ingestor) doRequest(ctx context.Context, req *http.Request) (io.ReadCloser, error) {
resp, err := rdi.cli.Do(ctx, req)
if err == nil {
// Early-exit on success
return resp.Body, nil
}
if fallbackBody := errorToFallback(err, rdi.fallback); fallbackBody != nil {
// the go-github REST API has a funny way of returning HTTP status codes,
// on a non-200 status it will return a github.ErrorResponse
// whereas the standard library will return nil error and the HTTP status code in the response
return fallbackBody, nil
}
return nil, fmt.Errorf("cannot make request: %w", err)
}
func errorToFallback(err error, fallback []ingestorFallback) io.ReadCloser {
var respErr *github.ErrorResponse
if errors.As(err, &respErr) {
if respErr.Response != nil {
return httpStatusToFallback(respErr.Response.StatusCode, fallback)
}
}
return nil
}
func httpStatusToFallback(httpStatus int, fallback []ingestorFallback) io.ReadCloser {
for _, fb := range fallback {
if fb.httpCode == httpStatus {
zerolog.Ctx(context.Background()).Debug().Msgf("falling back to body [%s]", fb.body)
return io.NopCloser(strings.NewReader(fb.body))
}
}
return nil
}
func (rdi *Ingestor) parseBody(body io.Reader) (any, error) {
var data any
var err error
if body == nil {
return nil, nil
}
if rdi.restCfg.Parse == "json" {
var jsonData any
dec := json.NewDecoder(body)
if err := dec.Decode(&jsonData); err != nil {
return nil, fmt.Errorf("cannot decode json: %w", err)
}
data = jsonData
} else {
data, err = io.ReadAll(body)
if err != nil {
return nil, fmt.Errorf("cannot read response body: %w", err)
}
}
return data, nil
}