// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: BUSL-1.1 package generate import ( "context" "errors" "fmt" "log/slog" "os" "path/filepath" "slices" "github.com/Masterminds/semver" "github.com/hashicorp/hcl/v2/gohcl" "github.com/hashicorp/hcl/v2/hclwrite" "github.com/hashicorp/vault/tools/pipeline/internal/pkg/metadata" "github.com/hashicorp/vault/tools/pipeline/internal/pkg/releases" slogctx "github.com/veqryn/slog-context" ) // EnosDynamicConfigReq is a request to generate dynamic enos configuration type EnosDynamicConfigReq struct { VaultEdition string VaultVersion string EnosDir string FileName string Skip []string NMinus uint releases.VersionLister } // EnosDynamicConfigRes is a response from a request to generate dynamic enos configuration type EnosDynamicConfigRes struct { Globals *Globals `json:"globals,omitempty" hcl:"globals,block" cty:"globals"` } // Globals are our dynamic globals type Globals struct { SampleAttributes *SampleAttrs `json:"sample_attributes,omitempty" hcl:"sample_attributes" cty:"sample_attributes"` } // SampleAttrs are the dynamic sample attributes that we'll write as globals type SampleAttrs struct { AWSRegion []string `json:"aws_region,omitempty" hcl:"aws_region" cty:"aws_region"` DistroVersionAmzn []string `json:"distro_version_amzn,omitempty" hcl:"distro_version_amzn" cty:"distro_version_amzn"` DistroVersionLeap []string `json:"distro_version_leap,omitempty" hcl:"distro_version_leap" cty:"distro_version_leap"` DistroVersionRhel []string `json:"distro_version_rhel,omitempty" hcl:"distro_version_rhel" cty:"distro_version_rhel"` DistroVersionSles []string `json:"distro_version_sles,omitempty" hcl:"distro_version_sles" cty:"distro_version_sles"` DistroVersionUbuntu []string `json:"distro_version_ubuntu,omitempty" hcl:"distro_version_ubuntu" cty:"distro_version_ubuntu"` UpgradeInitialVersion []string `json:"upgrade_initial_version,omitempty" hcl:"upgrade_initial_version" cty:"upgrade_initial_version"` } // Validate validates the request parameters func (e *EnosDynamicConfigReq) Validate(ctx context.Context) error { if e == nil { return errors.New("enos dynamic config req: validate: uninitialized") } slog.Default().DebugContext(ctx, "validating enos dynamic config request") if e.FileName == "" { return errors.New("no destination file name set") } if e.VersionLister == nil { return errors.New("no version lister set") } if !slices.Contains(metadata.Editions, e.VaultEdition) { return fmt.Errorf("unknown edition: %s", e.VaultEdition) } _, err := semver.NewVersion(e.VaultVersion) if err != nil { return fmt.Errorf("invalid version: %s: %w", e.VaultVersion, err) } s, err := os.Stat(e.EnosDir) if err != nil { return fmt.Errorf("invalid enos dir: %s: %w", e.EnosDir, err) } if !s.IsDir() { return fmt.Errorf("invalid enos dir: %s is not a directory", e.EnosDir) } return nil } // Run runs the dynamic configuration request func (e *EnosDynamicConfigReq) Run(ctx context.Context) (*EnosDynamicConfigRes, error) { if e == nil { return nil, fmt.Errorf("enos-dynamic-config-req uninitialized") } ctx = slogctx.Append(ctx, slog.String("vault-edition", e.VaultEdition), slog.String("vault-version", e.VaultVersion), slog.String("file-name", e.FileName), slog.String("dir", e.EnosDir), slog.Uint64("n-minnux", uint64(e.NMinus)), "skip", e.Skip, ) slog.Default().DebugContext(ctx, "running enos dynamic config request") err := e.Validate(ctx) if err != nil { return nil, err } res := &EnosDynamicConfigRes{} res.Globals, err = e.getGlobals(ctx) if err != nil { return nil, err } return res, e.writeFile(ctx, res) } func (e *EnosDynamicConfigReq) getGlobals(ctx context.Context) (*Globals, error) { var err error res := &Globals{} res.SampleAttributes, err = e.getSampleAttrs(ctx) return res, err } func (e *EnosDynamicConfigReq) getSampleAttrs(ctx context.Context) (*SampleAttrs, error) { // Create our HCL body attrs := &SampleAttrs{ // Use the cheapest regions AWSRegion: []string{"us-east-1", "us-west-2"}, // Current distro defaults DistroVersionAmzn: []string{"2023"}, DistroVersionLeap: []string{"15.6"}, DistroVersionRhel: []string{"8.10", "9.5"}, DistroVersionSles: []string{"15.6"}, DistroVersionUbuntu: []string{"22.04", "24.04"}, } // Create our initial upgrade version list. We'll find all released versions between N-3 -> Current // version, minus any explicitly skipped versions that have been set. Since CE and Ent do not share // the same version lineage now we'll also have to figure that in as well. versionReq := &releases.ListVersionsReq{ VersionLister: e.VersionLister, LicenseClass: e.VaultEdition, UpperBound: e.VaultVersion, NMinus: e.NMinus, Skip: e.Skip, } versionRes, err := versionReq.Run(ctx) if err != nil { return nil, err } attrs.UpgradeInitialVersion = versionRes.Versions return attrs, nil } // writeFile creates the dynamic config file and writes the dynamic data into it func (e *EnosDynamicConfigReq) writeFile(ctx context.Context, res *EnosDynamicConfigRes) error { select { case <-ctx.Done(): return ctx.Err() default: } slog.Default().DebugContext(ctx, "writing enos dynamic config request") // Make sure our path is valid path, err := filepath.Abs(filepath.Join(e.EnosDir, e.FileName)) if err != nil { return fmt.Errorf("expanding path dynamic config request path: %w", err) } f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o644) if err != nil { return fmt.Errorf("opening dynamic config request destination file: %w", err) } defer f.Close() if _, err = f.WriteString(`# Copyright (c) HashiCorp, Inc. # SPDX-License-Identifier: BUSL-1.1 # Code generated by pipeline generate enos-dynamic-config DO NOT EDIT. # This file is overwritten in CI as it contains branch specific and sometimes ever-changing values. # It's checked in here so that enos samples and scenarios can be performed, just be aware that this # might change out from under you. `); err != nil { return err } hf := hclwrite.NewEmptyFile() gohcl.EncodeIntoBody(res, hf.Body()) bytes := hclwrite.Format(hf.Bytes()) slog.Default().InfoContext(ctx, "writing enos dynamic config request", "path", path, "hcl", string(bytes), ) _, err = f.Write(bytes) return err }