fleet/pkg/spec/spec.go
Victor Lyuboslavsky 383d03cd6d
GitOps support for NDES SCEP proxy config. (#22918)
#22124

# Demo
<div>
<a href="https://www.loom.com/share/ad30efda816e4bf3abca0eab92b6e928">
      <p>[Demo] NDES SCEP proxy gitops #21955 - Watch Video</p>
    </a>
<a href="https://www.loom.com/share/ad30efda816e4bf3abca0eab92b6e928">
<img style="max-width:300px;"
src="https://cdn.loom.com/sessions/thumbnails/ad30efda816e4bf3abca0eab92b6e928-33b1f073da294133-full-play.gif">
    </a>
  </div>

# Checklist for submitter
- [x] Added/updated tests
- [x] Manual QA for all new/changed functionality
2024-10-16 11:12:48 -05:00

214 lines
6.3 KiB
Go

// Package spec contains functionality to parse "Fleet specs" yaml files
// (which are concatenated yaml files) that can be applied to a Fleet server.
package spec
import (
"crypto/rand"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"os"
"regexp"
"strings"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/ghodss/yaml"
"github.com/hashicorp/go-multierror"
)
var yamlSeparator = regexp.MustCompile(`(?m:^---[\t ]*)`)
// Group holds a set of "specs" that can be applied to a Fleet server.
type Group struct {
Queries []*fleet.QuerySpec
Teams []json.RawMessage
Packs []*fleet.PackSpec
Labels []*fleet.LabelSpec
Policies []*fleet.PolicySpec
Software []*fleet.SoftwarePackageSpec
// This needs to be interface{} to allow for the patch logic. Otherwise we send a request that looks to the
// server like the user explicitly set the zero values.
AppConfig interface{}
EnrollSecret *fleet.EnrollSecretSpec
UsersRoles *fleet.UsersRoleSpec
TeamsDryRunAssumptions *fleet.TeamSpecsDryRunAssumptions
}
// Metadata holds the metadata for a single YAML section/item.
type Metadata struct {
Kind string `json:"kind"`
Version string `json:"apiVersion"`
Spec json.RawMessage `json:"spec"`
}
// GroupFromBytes parses a Group from concatenated YAML specs.
func GroupFromBytes(b []byte) (*Group, error) {
specs := &Group{}
for _, specItem := range SplitYaml(string(b)) {
var s Metadata
if err := yaml.Unmarshal([]byte(specItem), &s); err != nil {
return nil, fmt.Errorf("failed to unmarshal spec item %w: \n%s", err, specItem)
}
kind := strings.ToLower(s.Kind)
if s.Spec == nil {
if kind == "" {
return nil, errors.New(`Missing required fields ("spec", "kind") on provided configuration.`)
}
return nil, fmt.Errorf(`Missing required fields ("spec") on provided %q configuration.`, s.Kind)
}
switch kind {
case fleet.QueryKind:
var querySpec *fleet.QuerySpec
if err := yaml.Unmarshal(s.Spec, &querySpec); err != nil {
return nil, fmt.Errorf("unmarshaling %s spec: %w", kind, err)
}
specs.Queries = append(specs.Queries, querySpec)
case fleet.PackKind:
var packSpec *fleet.PackSpec
if err := yaml.Unmarshal(s.Spec, &packSpec); err != nil {
return nil, fmt.Errorf("unmarshaling %s spec: %w", kind, err)
}
specs.Packs = append(specs.Packs, packSpec)
case fleet.LabelKind:
var labelSpec *fleet.LabelSpec
if err := yaml.Unmarshal(s.Spec, &labelSpec); err != nil {
return nil, fmt.Errorf("unmarshaling %s spec: %w", kind, err)
}
specs.Labels = append(specs.Labels, labelSpec)
case fleet.PolicyKind:
var policySpec *fleet.PolicySpec
if err := yaml.Unmarshal(s.Spec, &policySpec); err != nil {
return nil, fmt.Errorf("unmarshaling %s spec: %w", kind, err)
}
specs.Policies = append(specs.Policies, policySpec)
case fleet.AppConfigKind:
if specs.AppConfig != nil {
return nil, errors.New("config defined twice in the same file")
}
var appConfigSpec interface{}
if err := yaml.Unmarshal(s.Spec, &appConfigSpec); err != nil {
return nil, fmt.Errorf("unmarshaling %s spec: %w", kind, err)
}
specs.AppConfig = appConfigSpec
case fleet.EnrollSecretKind:
if specs.AppConfig != nil {
return nil, errors.New("enroll_secret defined twice in the same file")
}
var enrollSecretSpec *fleet.EnrollSecretSpec
if err := yaml.Unmarshal(s.Spec, &enrollSecretSpec); err != nil {
return nil, fmt.Errorf("unmarshaling %s spec: %w", kind, err)
}
specs.EnrollSecret = enrollSecretSpec
case fleet.UserRolesKind:
var userRoleSpec *fleet.UsersRoleSpec
if err := yaml.Unmarshal(s.Spec, &userRoleSpec); err != nil {
return nil, fmt.Errorf("unmarshaling %s spec: %w", kind, err)
}
specs.UsersRoles = userRoleSpec
case fleet.TeamKind:
// unmarshal to a raw map as we don't want to strip away unknown/invalid
// fields at this point - that validation is done in the apply spec/teams
// endpoint so that it is enforced for both the API and the CLI.
rawTeam := make(map[string]json.RawMessage)
if err := yaml.Unmarshal(s.Spec, &rawTeam); err != nil {
return nil, fmt.Errorf("unmarshaling %s spec: %w", kind, err)
}
specs.Teams = append(specs.Teams, rawTeam["team"])
default:
return nil, fmt.Errorf("unknown kind %q", s.Kind)
}
}
return specs, nil
}
// SplitYaml splits a text file into separate yaml documents divided by ---
func SplitYaml(in string) []string {
var out []string
for _, chunk := range yamlSeparator.Split(in, -1) {
chunk = strings.TrimSpace(chunk)
if chunk == "" {
continue
}
out = append(out, chunk)
}
return out
}
func generateRandomString(sizeBytes int) string {
b := make([]byte, sizeBytes)
if _, err := rand.Read(b); err != nil {
panic(err)
}
return hex.EncodeToString(b)
}
func ExpandEnv(s string) (string, error) {
// Generate a random escaping prefix that doesn't exist in s.
var preventEscapingPrefix string
for {
preventEscapingPrefix = "PREVENT_ESCAPING_" + generateRandomString(8)
if !strings.Contains(s, preventEscapingPrefix) {
break
}
}
s = escapeString(s, preventEscapingPrefix)
s = escapeFleetVar(s, preventEscapingPrefix)
var err *multierror.Error
s = os.Expand(s, func(env string) string {
if strings.HasPrefix(env, preventEscapingPrefix) {
return "$" + strings.TrimPrefix(env, preventEscapingPrefix)
}
v, ok := os.LookupEnv(env)
if !ok {
err = multierror.Append(err, fmt.Errorf("environment variable %q not set", env))
return ""
}
return v
})
if err != nil {
return "", err
}
return s, nil
}
func ExpandEnvBytes(b []byte) ([]byte, error) {
s, err := ExpandEnv(string(b))
if err != nil {
return nil, err
}
return []byte(s), nil
}
var escapePattern = regexp.MustCompile(`(\\+\$)`)
func escapeString(s string, preventEscapingPrefix string) string {
return escapePattern.ReplaceAllStringFunc(s, func(match string) string {
if len(match)%2 != 0 {
return match
}
return strings.Repeat("\\", (len(match)/2)-1) + "$" + preventEscapingPrefix
})
}
var escapeFleetVarPattern = regexp.MustCompile(`(\$FLEET_VAR_\w+)|(\${FLEET_VAR_\w+})`)
func escapeFleetVar(s string, preventEscapingPrefix string) string {
return escapeFleetVarPattern.ReplaceAllStringFunc(s, func(match string) string {
return strings.ReplaceAll(match, "$", "$"+preventEscapingPrefix)
})
}