Skip to content

Commit

Permalink
Config Generation: Structured Result (#1677)
Browse files Browse the repository at this point in the history
This result is something we can build upon and it allows the following:
- Bubbling up multiple errors: Resource A and Resource B failed, but resource C worked
- Ignoring or treating differently some errors (by casting). For example, ResourceErrors are not critical because they only mean that a single resource failed
- Building up a single result object as we go along. Collecting success or errors from multiple parts of the generation process
  • Loading branch information
julienduchesne authored Jul 11, 2024
1 parent f39fd6f commit a8bb914
Show file tree
Hide file tree
Showing 6 changed files with 162 additions and 75 deletions.
4 changes: 3 additions & 1 deletion cmd/generate/main.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"errors"
"fmt"
"log"
"os"
Expand Down Expand Up @@ -167,7 +168,8 @@ This supports a glob format. Examples:
if err != nil {
return fmt.Errorf("failed to parse flags: %w", err)
}
return generate.Generate(ctx.Context, cfg)
result := generate.Generate(ctx.Context, cfg)
return errors.Join(result.Errors...)
},
}

Expand Down
8 changes: 1 addition & 7 deletions internal/resources/slo/resource_slo.go
Original file line number Diff line number Diff line change
Expand Up @@ -272,13 +272,7 @@ func listSlos(ctx context.Context, client *common.Client, data any) ([]string, e

slolist, _, err := sloClient.DefaultAPI.V1SloGet(ctx).Execute()
if err != nil {
// // TODO: Uninitialized SLO plugin. This should be handled better
// cast, ok := err.(*slo.GenericOpenAPIError)
// if ok && strings.Contains(cast.Error(), "status: 500") {
// return nil, nil
// }

return nil, nil
return nil, err
}

var ids []string
Expand Down
35 changes: 18 additions & 17 deletions pkg/generate/cloud.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,32 +33,32 @@ type stack struct {
onCallToken string
}

func generateCloudResources(ctx context.Context, cfg *Config) ([]stack, error) {
func generateCloudResources(ctx context.Context, cfg *Config) ([]stack, GenerationResult) {
// Gen provider
providerBlock := hclwrite.NewBlock("provider", []string{"grafana"})
providerBlock.Body().SetAttributeValue("alias", cty.StringVal("cloud"))
providerBlock.Body().SetAttributeValue("cloud_access_policy_token", cty.StringVal(cfg.Cloud.AccessPolicyToken))
if err := writeBlocks(filepath.Join(cfg.OutputDir, "cloud-provider.tf"), providerBlock); err != nil {
return nil, err
return nil, failure(err)
}

// Generate imports
config := provider.ProviderConfig{
CloudAccessPolicyToken: types.StringValue(cfg.Cloud.AccessPolicyToken),
}
if err := config.SetDefaults(); err != nil {
return nil, err
return nil, failure(err)
}

client, err := provider.CreateClients(config)
if err != nil {
return nil, err
return nil, failure(err)
}
cloudClient := client.GrafanaCloudAPI

stacks, _, err := cloudClient.InstancesAPI.GetInstances(ctx).Execute()
if err != nil {
return nil, err
return nil, failure(err)
}

// Cleanup SAs
Expand All @@ -67,32 +67,33 @@ func generateCloudResources(ctx context.Context, cfg *Config) ([]stack, error) {
if cfg.Cloud.CreateStackServiceAccount {
for _, stack := range stacks.Items {
if err := createManagementStackServiceAccount(ctx, cloudClient, stack, managementServiceAccountName); err != nil {
return nil, err
return nil, failure(err)
}
}
}

data := cloud.NewListerData(cfg.Cloud.Org)
if err := generateImportBlocks(ctx, client, data, cloud.Resources, cfg, "cloud"); err != nil {
return nil, err
returnResult := generateImportBlocks(ctx, client, data, cloud.Resources, cfg, "cloud")
if returnResult.Blocks() == 0 { // Skip if no resources were found
return nil, returnResult
}

plannedState, err := getPlannedState(ctx, cfg)
if err != nil {
return nil, err
return nil, failure(err)
}
if err := postprocessing.StripDefaults(filepath.Join(cfg.OutputDir, "cloud-resources.tf"), nil); err != nil {
return nil, err
return nil, failure(err)
}
if err := postprocessing.WrapJSONFieldsInFunction(filepath.Join(cfg.OutputDir, "cloud-resources.tf")); err != nil {
return nil, err
return nil, failure(err)
}
if err := postprocessing.ReplaceReferences(filepath.Join(cfg.OutputDir, "cloud-resources.tf"), plannedState, nil); err != nil {
return nil, err
return nil, failure(err)
}

if !cfg.Cloud.CreateStackServiceAccount {
return nil, nil
return nil, returnResult
}

// Add management service account (grafana_cloud_stack_service_account)
Expand Down Expand Up @@ -149,7 +150,7 @@ func generateCloudResources(ctx context.Context, cfg *Config) ([]stack, error) {
providerBlock.Body().SetAttributeTraversal("sm_url", traversal("grafana_synthetic_monitoring_installation", stack.Slug, "stack_sm_api_url"))

if err := writeBlocks(filepath.Join(cfg.OutputDir, fmt.Sprintf("stack-%s-provider.tf", stack.Slug)), saBlock, saTokenBlock, smInstallationMetricsPublishBlock, smInstallationTokenBlock, smInstallationBlock, providerBlock); err != nil {
return nil, fmt.Errorf("failed to write management service account blocks for stack %q: %w", stack.Slug, err)
return nil, failuref("failed to write management service account blocks for stack %q: %w", stack.Slug, err)
}

// Apply then go into the state and find the management key
Expand All @@ -161,14 +162,14 @@ func generateCloudResources(ctx context.Context, cfg *Config) ([]stack, error) {
tfexec.Target("grafana_synthetic_monitoring_installation."+stack.Slug),
)
if err != nil {
return nil, fmt.Errorf("failed to apply management service account blocks for stack %q: %w", stack.Slug, err)
return nil, failuref("failed to apply management service account blocks for stack %q: %w", stack.Slug, err)
}
}

managedStacks := []stack{}
state, err := getState(ctx, cfg)
if err != nil {
return nil, err
return nil, failure(err)
}
stacksMap := map[string]stack{}
for _, resource := range state.Values.RootModule.Resources {
Expand Down Expand Up @@ -198,7 +199,7 @@ func generateCloudResources(ctx context.Context, cfg *Config) ([]stack, error) {
managedStacks = append(managedStacks, stack)
}

return managedStacks, nil
return managedStacks, returnResult
}

func createManagementStackServiceAccount(ctx context.Context, cloudClient *gcom.APIClient, stack gcom.FormattedApiInstance, saName string) error {
Expand Down
130 changes: 94 additions & 36 deletions pkg/generate/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,26 +22,65 @@ var (
allowedTerraformChars = regexp.MustCompile(`[^a-zA-Z0-9_-]`)
)

func Generate(ctx context.Context, cfg *Config) error {
// ResourceError is an error that occurred while generating a resource.
// It can be filtered out by the caller if it is not critical that a single resource failed.
type ResourceError struct {
Resource *common.Resource
Err error
}

func (e ResourceError) Error() string {
return fmt.Sprintf("resource %s: %v", e.Resource.Name, e.Err)
}

type GenerationSuccess struct {
Resource *common.Resource
Blocks int
}

type GenerationResult struct {
Success []GenerationSuccess
Errors []error
}

func (r GenerationResult) Blocks() int {
blocks := 0
for _, s := range r.Success {
blocks += s.Blocks
}
return blocks
}

func failure(err error) GenerationResult {
return GenerationResult{
Errors: []error{err},
}
}

func failuref(format string, args ...any) GenerationResult {
return failure(fmt.Errorf(format, args...))
}

func Generate(ctx context.Context, cfg *Config) GenerationResult {
var err error
if !filepath.IsAbs(cfg.OutputDir) {
if cfg.OutputDir, err = filepath.Abs(cfg.OutputDir); err != nil {
return fmt.Errorf("failed to get absolute path for %s: %w", cfg.OutputDir, err)
return failuref("failed to get absolute path for %s: %w", cfg.OutputDir, err)
}
}

if _, err := os.Stat(cfg.OutputDir); err == nil && cfg.Clobber {
log.Printf("Deleting all files in %s", cfg.OutputDir)
if err := os.RemoveAll(cfg.OutputDir); err != nil {
return fmt.Errorf("failed to delete %s: %s", cfg.OutputDir, err)
return failuref("failed to delete %s: %s", cfg.OutputDir, err)
}
} else if err == nil && !cfg.Clobber {
return fmt.Errorf("output dir %q already exists. Use the clobber option to delete it", cfg.OutputDir)
return failuref("output dir %q already exists. Use the clobber option to delete it", cfg.OutputDir)
}

log.Printf("Generating resources to %s", cfg.OutputDir)
if err := os.MkdirAll(cfg.OutputDir, 0755); err != nil {
return fmt.Errorf("failed to create output directory %s: %s", cfg.OutputDir, err)
return failuref("failed to create output directory %s: %s", cfg.OutputDir, err)
}

// Generate provider installation block
Expand All @@ -59,22 +98,21 @@ func Generate(ctx context.Context, cfg *Config) error {
tf, err := setupTerraform(cfg)
// Terraform init to download the provider
if err != nil {
return fmt.Errorf("failed to run terraform init: %w", err)
return failuref("failed to run terraform init: %w", err)
}
cfg.Terraform = tf

var returnResult GenerationResult
if cfg.Cloud != nil {
log.Printf("Generating cloud resources")
stacks, err := generateCloudResources(ctx, cfg)
if err != nil {
return err
}
var stacks []stack
stacks, returnResult = generateCloudResources(ctx, cfg)

for _, stack := range stacks {
stack.name = "stack-" + stack.slug
if err := generateGrafanaResources(ctx, cfg, stack, false); err != nil {
return err
}
stackResult := generateGrafanaResources(ctx, cfg, stack, false)
returnResult.Success = append(returnResult.Success, stackResult.Success...)
returnResult.Errors = append(returnResult.Errors, stackResult.Errors...)
}
}

Expand All @@ -89,29 +127,42 @@ func Generate(ctx context.Context, cfg *Config) error {
onCallURL: cfg.Grafana.OnCallURL,
}
log.Printf("Generating Grafana resources")
if err := generateGrafanaResources(ctx, cfg, stack, true); err != nil {
return err
returnResult = generateGrafanaResources(ctx, cfg, stack, true)
}

if !cfg.OutputCredentials && cfg.Format != OutputFormatCrossplane {
if err := postprocessing.RedactCredentials(cfg.OutputDir); err != nil {
return failuref("failed to redact credentials: %w", err)
}
}

if cfg.Format == OutputFormatCrossplane {
return convertToCrossplane(cfg)
if returnResult.Blocks() == 0 {
if err := os.WriteFile(filepath.Join(cfg.OutputDir, "resources.tf"), []byte("# No resources were found\n"), 0600); err != nil {
return failure(err)
}
if err := os.WriteFile(filepath.Join(cfg.OutputDir, "imports.tf"), []byte("# No resources were found\n"), 0600); err != nil {
return failure(err)
}
return returnResult
}

if !cfg.OutputCredentials {
if err := postprocessing.RedactCredentials(cfg.OutputDir); err != nil {
return fmt.Errorf("failed to redact credentials: %w", err)
if cfg.Format == OutputFormatCrossplane {
if err := convertToCrossplane(cfg); err != nil {
return failure(err)
}
return returnResult
}

if cfg.Format == OutputFormatJSON {
return convertToTFJSON(cfg.OutputDir)
if err := convertToTFJSON(cfg.OutputDir); err != nil {
return failure(err)
}
}

return nil
return returnResult
}

func generateImportBlocks(ctx context.Context, client *common.Client, listerData any, resources []*common.Resource, cfg *Config, provider string) error {
func generateImportBlocks(ctx context.Context, client *common.Client, listerData any, resources []*common.Resource, cfg *Config, provider string) GenerationResult {
generatedFilename := func(suffix string) string {
if provider == "" {
return filepath.Join(cfg.OutputDir, suffix)
Expand All @@ -122,7 +173,7 @@ func generateImportBlocks(ctx context.Context, client *common.Client, listerData

resources, err := filterResources(resources, cfg.IncludeResources)
if err != nil {
return err
return failure(err)
}

// Generate HCL blocks in parallel with a wait group
Expand Down Expand Up @@ -207,12 +258,21 @@ func generateImportBlocks(ctx context.Context, client *common.Client, listerData
wg.Wait()
close(results)

returnResult := GenerationResult{}
resultsSlice := []result{}
for r := range results {
if r.err != nil {
return fmt.Errorf("failed to generate %s resources: %w", r.resource.Name, r.err)
returnResult.Errors = append(returnResult.Errors, ResourceError{
Resource: r.resource,
Err: r.err,
})
} else {
resultsSlice = append(resultsSlice, r)
returnResult.Success = append(returnResult.Success, GenerationSuccess{
Resource: r.resource,
Blocks: len(r.blocks),
})
}
resultsSlice = append(resultsSlice, r)
}
sort.Slice(resultsSlice, func(i, j int) bool {
return resultsSlice[i].resource.Name < resultsSlice[j].resource.Name
Expand All @@ -225,23 +285,21 @@ func generateImportBlocks(ctx context.Context, client *common.Client, listerData
}

if len(allBlocks) == 0 {
if err := os.WriteFile(generatedFilename("resources.tf"), []byte("# No resources were found\n"), 0600); err != nil {
return err
}
if err := os.WriteFile(generatedFilename("imports.tf"), []byte("# No resources were found\n"), 0600); err != nil {
return err
}
return nil
return returnResult
}

if err := writeBlocks(generatedFilename("imports.tf"), allBlocks...); err != nil {
return err
return failure(err)
}
_, err = cfg.Terraform.Plan(ctx, tfexec.GenerateConfigOut(generatedFilename("resources.tf")))
if err != nil {
return fmt.Errorf("failed to generate resources: %w", err)
return failuref("failed to generate resources: %w", err)
}
return sortResourcesFile(generatedFilename("resources.tf"))
if err := sortResourcesFile(generatedFilename("resources.tf")); err != nil {
return failure(err)
}

return returnResult
}

func filterResources(resources []*common.Resource, includedResources []string) ([]*common.Resource, error) {
Expand Down
Loading

0 comments on commit a8bb914

Please sign in to comment.