Skip to content

Commit bbd0626

Browse files
authored
Merge pull request #65 from dpc-sdp/feature/fix-csv-export-include-facts
Added parameter for facts by type. Improved handling of API errors
2 parents 576eb49 + 5c40378 commit bbd0626

2 files changed

Lines changed: 125 additions & 9 deletions

File tree

cmd/project-metadata/metadata.go

Lines changed: 121 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ type ProjectMetadata struct {
2929
BackendProject string `json:"backend-project"`
3030
ProductionDomain string `json:"production-domain"`
3131
Facts map[string]string `json:"facts,omitempty"`
32+
IndividualFacts map[string]string `json:"individual_facts,omitempty"`
3233
}
3334

3435
// Fact represents a single fact from the Lagoon API
@@ -67,6 +68,21 @@ func parseTypes(typeStr string) []string {
6768
return types
6869
}
6970

71+
// parseFactTypes parses a comma-separated string of fact types and returns a slice of fact types
72+
func parseFactTypes(factTypesStr string) []string {
73+
if factTypesStr == "" {
74+
return []string{}
75+
}
76+
77+
// Split by comma and trim whitespace
78+
factTypes := strings.Split(factTypesStr, ",")
79+
for i, t := range factTypes {
80+
factTypes[i] = strings.TrimSpace(t)
81+
}
82+
83+
return factTypes
84+
}
85+
7086
// getProjectsByTypes fetches projects for multiple types
7187
func getProjectsByTypes(ctx context.Context, client *lagoon_client.Client, types []string) ([]string, error) {
7288
var allArgs []string
@@ -110,11 +126,16 @@ func Metadata(ctx context.Context, c *cli.Command) error {
110126
all := c.Bool("all")
111127
metadataType := c.String("type")
112128
includeFacts := c.Bool("include-facts")
129+
factTypesStr := c.String("fact-types")
113130
args := make([]string, 0)
114131

115132
// Parse the type parameter to handle comma-separated values
116133
types := parseTypes(metadataType)
117134

135+
// Parse the fact-types parameter to handle comma-separated values
136+
factTypes := parseFactTypes(factTypesStr)
137+
useIndividualFacts := len(factTypes) > 0
138+
118139
if all {
119140
// Get projects for all specified types
120141
projectArgs, err := getProjectsByTypes(ctx, client, types)
@@ -144,13 +165,27 @@ func Metadata(ctx context.Context, c *cli.Command) error {
144165
project := &schema.ProjectMetadata{}
145166
err := client.ProjectByNameMetadata(ctx, v, project)
146167
if err != nil {
168+
// Check if this is the throttling error we're looking for
169+
errStr := err.Error()
170+
if strings.Contains(errStr, "invalid character '<' looking for beginning of value") {
171+
return fmt.Errorf("API throttling detected - server returned HTML instead of JSON. Error: %v", err)
172+
} else if strings.Contains(errStr, "decoding response") {
173+
return fmt.Errorf("API response decoding error - possible throttling or server issue. Error: %v", err)
174+
}
147175
return err
148176
}
149177

150178
// Get extended project info to access productionEnvironment field
151179
extendedProject := &schema.Project{}
152180
err = client.ProjectByNameExtended(ctx, v, extendedProject)
153181
if err != nil {
182+
// Check if this is the throttling error we're looking for
183+
errStr := err.Error()
184+
if strings.Contains(errStr, "invalid character '<' looking for beginning of value") {
185+
return fmt.Errorf("API throttling detected during extended project fetch - server returned HTML instead of JSON. Error: %v", err)
186+
} else if strings.Contains(errStr, "decoding response") {
187+
return fmt.Errorf("API response decoding error during extended project fetch - possible throttling or server issue. Error: %v", err)
188+
}
154189
return err
155190
}
156191

@@ -164,9 +199,10 @@ func Metadata(ctx context.Context, c *cli.Command) error {
164199
ProductionDomain: project.Metadata["production-domain"],
165200
}
166201

167-
// Fetch facts if requested
168-
if includeFacts {
202+
// Fetch facts if requested or if individual fact types are specified
203+
if includeFacts || useIndividualFacts {
169204
facts := make(map[string]string)
205+
individualFacts := make(map[string]string)
170206

171207
// Use the specific GraphQL query to fetch facts from production environment
172208
query := `
@@ -199,7 +235,27 @@ func Metadata(ctx context.Context, c *cli.Command) error {
199235

200236
response, err := client.ProcessRaw(ctx, query, variables)
201237
if err != nil {
202-
facts["status"] = fmt.Sprintf("Unable to fetch facts: %v", err)
238+
// Capture full error details for debugging
239+
errStr := err.Error()
240+
facts["error_type"] = "ProcessRaw_Error"
241+
facts["full_error"] = errStr
242+
243+
// Check for specific error patterns
244+
if strings.Contains(errStr, "invalid character '<'") || strings.Contains(errStr, "decoding response") {
245+
facts["status"] = "API returned HTML error page - possible throttling or server error"
246+
// Try to extract more details from the error
247+
if strings.Contains(errStr, "invalid character '<' looking for beginning of value") {
248+
facts["likely_cause"] = "HTML_response_instead_of_JSON"
249+
}
250+
} else if strings.Contains(strings.ToLower(errStr), "throttl") || strings.Contains(strings.ToLower(errStr), "rate limit") {
251+
facts["status"] = "API throttling detected"
252+
facts["likely_cause"] = "Rate_limiting"
253+
} else if strings.Contains(strings.ToLower(errStr), "timeout") {
254+
facts["status"] = "Request timeout"
255+
facts["likely_cause"] = "Timeout"
256+
} else {
257+
facts["status"] = fmt.Sprintf("Unable to fetch facts: %v", err)
258+
}
203259
} else {
204260
// Parse the response
205261
responseBytes, err := json.Marshal(response)
@@ -219,24 +275,48 @@ func Metadata(ctx context.Context, c *cli.Command) error {
219275
// Extract facts from production environments
220276
if len(projectResponse.ProjectByName.Environments) > 0 {
221277
for _, env := range projectResponse.ProjectByName.Environments {
222-
for _, fact := range env.Facts {
223-
if fact.Name != "" && fact.Value != "" {
224-
facts[fact.Name] = fact.Value
278+
if env.Name == "production" || env.Name == "master" {
279+
for _, fact := range env.Facts {
280+
if fact.Name != "" && fact.Value != "" {
281+
// Always store in facts for backward compatibility
282+
if includeFacts {
283+
facts[fact.Name] = fact.Value
284+
}
285+
286+
// Store individual facts if specific types are requested
287+
if useIndividualFacts {
288+
for _, requestedType := range factTypes {
289+
if fact.Name == requestedType {
290+
individualFacts[fact.Name] = fact.Value
291+
break
292+
}
293+
}
294+
}
295+
}
225296
}
297+
} else {
298+
continue
226299
}
227300
}
228-
if len(facts) == 0 {
301+
if includeFacts && len(facts) == 0 {
229302
facts["status"] = "No facts available for production environment"
230303
}
231304
} else {
232-
facts["status"] = "No production environment found"
305+
if includeFacts {
306+
facts["status"] = "No production environment found"
307+
}
233308
}
234309
}
235310
}
236311
}
237312
}
238313

239-
item.Facts = facts
314+
if includeFacts {
315+
item.Facts = facts
316+
}
317+
if useIndividualFacts {
318+
item.IndividualFacts = individualFacts
319+
}
240320
}
241321

242322
output.Items = append(output.Items, item)
@@ -262,6 +342,11 @@ func Metadata(ctx context.Context, c *cli.Command) error {
262342
if includeFacts {
263343
header = append(header, "Facts")
264344
}
345+
if useIndividualFacts {
346+
for _, factType := range factTypes {
347+
header = append(header, factType)
348+
}
349+
}
265350
writer.Write(header)
266351

267352
// Write CSV data rows
@@ -287,6 +372,17 @@ func Metadata(ctx context.Context, c *cli.Command) error {
287372
}
288373
record = append(record, factsStr)
289374
}
375+
if useIndividualFacts {
376+
for _, factType := range factTypes {
377+
value := ""
378+
if item.IndividualFacts != nil {
379+
if val, exists := item.IndividualFacts[factType]; exists {
380+
value = val
381+
}
382+
}
383+
record = append(record, value)
384+
}
385+
}
290386
writer.Write(record)
291387
}
292388
} else {
@@ -304,6 +400,11 @@ func Metadata(ctx context.Context, c *cli.Command) error {
304400
if includeFacts {
305401
headerCells = append(headerCells, &simpletable.Cell{Align: simpletable.AlignLeft, Text: "Facts"})
306402
}
403+
if useIndividualFacts {
404+
for _, factType := range factTypes {
405+
headerCells = append(headerCells, &simpletable.Cell{Align: simpletable.AlignLeft, Text: factType})
406+
}
407+
}
307408
table.Header = &simpletable.Header{
308409
Cells: headerCells,
309410
}
@@ -330,6 +431,17 @@ func Metadata(ctx context.Context, c *cli.Command) error {
330431
}
331432
r = append(r, &simpletable.Cell{Text: factsStr})
332433
}
434+
if useIndividualFacts {
435+
for _, factType := range factTypes {
436+
value := ""
437+
if item.IndividualFacts != nil {
438+
if val, exists := item.IndividualFacts[factType]; exists {
439+
value = val
440+
}
441+
}
442+
r = append(r, &simpletable.Cell{Text: value})
443+
}
444+
}
333445
table.Body.Cells = append(table.Body.Cells, r)
334446
}
335447
table.SetStyle(simpletable.StyleCompactLite)

main.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,10 @@ func main() {
115115
Name: "include-facts",
116116
Usage: "Include project facts (base image, Drupal version, etc.)",
117117
},
118+
&cli.StringFlag{
119+
Name: "fact-types",
120+
Usage: "Filter facts by type and display in separate columns (comma-separated: PHP,Drupal,Alpine Linux,etc.)",
121+
},
118122
&cli.StringFlag{
119123
Name: "output",
120124
Usage: "Output format - supports json, table, csv",

0 commit comments

Comments
 (0)