Skip to content

Commit

Permalink
[cmd/mdatagen]: Add feature gates support to metadata-schema.yaml
Browse files Browse the repository at this point in the history
  • Loading branch information
narcis96 committed Oct 21, 2024
1 parent a7d019f commit 95dc2e8
Show file tree
Hide file tree
Showing 5 changed files with 215 additions and 5 deletions.
14 changes: 9 additions & 5 deletions cmd/mdatagen/internal/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ func run(ymlPath string) error {
tmplDir := "templates"

codeDir := filepath.Join(ymlDir, "internal", md.GeneratedPackageName)
if err = os.MkdirAll(codeDir, 0700); err != nil {
if err = os.MkdirAll(codeDir, 0o700); err != nil {
return fmt.Errorf("unable to create output directory %q: %w", codeDir, err)
}
if md.Status != nil {
Expand Down Expand Up @@ -120,6 +120,10 @@ func run(ymlPath string) error {
toGenerate[filepath.Join(tmplDir, "documentation.md.tmpl")] = filepath.Join(ymlDir, "documentation.md")
}

if len(md.FeatureGates) != 0 {
toGenerate[filepath.Join(tmplDir, "feature_gates.go.tmpl")] = filepath.Join(ymlDir, "generated_feature_gates.go")
}

for tmpl, dst := range toGenerate {
if err = generateFile(tmpl, dst, md, "metadata"); err != nil {
return err
Expand All @@ -130,7 +134,7 @@ func run(ymlPath string) error {
return nil
}

if err = os.MkdirAll(filepath.Join(codeDir, "testdata"), 0700); err != nil {
if err = os.MkdirAll(filepath.Join(codeDir, "testdata"), 0o700); err != nil {
return fmt.Errorf("unable to create output directory %q: %w", filepath.Join(codeDir, "testdata"), err)
}

Expand Down Expand Up @@ -378,7 +382,7 @@ func inlineReplace(tmplFile string, outputFile string, md Metadata, start string
return err
}

var re = regexp.MustCompile(fmt.Sprintf("%s[\\s\\S]*%s", start, end))
re := regexp.MustCompile(fmt.Sprintf("%s[\\s\\S]*%s", start, end))
if !re.Match(readmeContents) {
return nil
}
Expand All @@ -393,7 +397,7 @@ func inlineReplace(tmplFile string, outputFile string, md Metadata, start string
}

s := re.ReplaceAllString(string(readmeContents), string(buf))
if err := os.WriteFile(outputFile, []byte(s), 0600); err != nil {
if err := os.WriteFile(outputFile, []byte(s), 0o600); err != nil {
return fmt.Errorf("failed writing %q: %w", outputFile, err)
}

Expand All @@ -418,7 +422,7 @@ func generateFile(tmplFile string, outputFile string, md Metadata, goPackage str
}
}

if err := os.WriteFile(outputFile, result, 0600); err != nil {
if err := os.WriteFile(outputFile, result, 0o600); err != nil {
return fmt.Errorf("failed writing %q: %w", outputFile, err)
}

Expand Down
78 changes: 78 additions & 0 deletions cmd/mdatagen/internal/loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"fmt"
"os/exec"
"path/filepath"
"regexp"
"strings"

"go.opentelemetry.io/collector/component"
Expand All @@ -20,6 +21,20 @@ import (
"go.opentelemetry.io/collector/pdata/pcommon"
)

var (
// idRegexp is used to validate the ID of a Gate.
// IDs' characters must be alphanumeric or dots.
idRegexp = regexp.MustCompile(`^[0-9a-zA-Z\.]*$`)
versionRegexp = regexp.MustCompile(`^v(\d+)\.(\d+)\.(\d+)$`)
referenceURLRegexp = regexp.MustCompile(`^(https?:\/\/)?([a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)+)(\/[^\s]*)?$`)
validStages = map[string]bool{
"Alpha": true,
"Beta": true,
"Stable": true,
"Deprecated": true,
}
)

type MetricName string

func (mn MetricName) Render() (string, error) {
Expand All @@ -40,6 +55,16 @@ func (mn AttributeName) RenderUnexported() (string, error) {
return FormatIdentifier(string(mn), false)
}

type featureGateName string

func (mn featureGateName) Render() (string, error) {
return FormatIdentifier(string(mn), true)
}

func (mn featureGateName) RenderUnexported() (string, error) {
return FormatIdentifier(string(mn), false)
}

// ValueType defines an attribute value type.
type ValueType struct {
// ValueType is type of the attribute value.
Expand Down Expand Up @@ -159,6 +184,7 @@ func (m *Metric) Unmarshal(parser *confmap.Conf) error {
}
return parser.Unmarshal(m)
}

func (m Metric) Data() MetricData {
if m.Sum != nil {
return m.Sum
Expand Down Expand Up @@ -296,6 +322,8 @@ type Metadata struct {
ShortFolderName string `mapstructure:"-"`
// Tests is the set of tests generated with the component
Tests tests `mapstructure:"tests"`
// FeatureGates that can be used for the component.
FeatureGates map[featureGateName]featureGate `mapstructure:"feature_gates"`
}

func setAttributesFullName(attrs map[AttributeName]Attribute) {
Expand Down Expand Up @@ -373,3 +401,53 @@ func packageName() (string, error) {
}
return strings.TrimSpace(string(output)), nil
}

type featureGate struct {
// Required.
ID string `mapstructure:"id"`
// Description describes the purpose of the attribute.
Description string `mapstructure:"description"`
// Stage current stage at which the feature gate is in the development lifecyle
Stage string `mapstructure:"stage"`
// ReferenceURL can optionally give the url of the feature_gate
ReferenceURL string `mapstructure:"reference_url"`
// FromVersion optional field which gives the release version from which the gate has been given the current stage
FromVersion string `mapstructure:"from_version"`
// ToVersion optional field which gives the release version till which the gate the gate had the given lifecycle stage
ToVersion string `mapstructure:"to_version"`
// FeatureGateName name of the feature gate
FeatureGateName featureGateName `mapstructure:"-"`
}

func (f *featureGate) validate(parser *confmap.Conf) error {
var err []error
if !parser.IsSet("id") {
err = append(err, errors.New("missing required field: `id`"))
} else if !idRegexp.MatchString(fmt.Sprintf("%v", parser.Get("id"))) {
err = append(err, fmt.Errorf("invalid character(s) in ID"))
}

if !parser.IsSet("stage") {
err = append(err, errors.New("missing required field: `stage`"))
} else if _, ok := validStages[fmt.Sprintf("%v", parser.Get("stage"))]; !ok {
err = append(err, fmt.Errorf("invalid stage"))
}

if parser.IsSet("from_version") && !versionRegexp.MatchString(fmt.Sprintf("%v", parser.Get("from_version"))) {
err = append(err, fmt.Errorf("invalid character(s) in from_version"))
}
if parser.IsSet("to_version") && !versionRegexp.MatchString(fmt.Sprintf("%v", parser.Get("to_version"))) {
err = append(err, fmt.Errorf("invalid character(s) in to_version"))
}
if parser.IsSet("reference_url") && !referenceURLRegexp.MatchString(fmt.Sprintf("%v", parser.Get("reference_url"))) {
err = append(err, fmt.Errorf("invalid character(s) in reference_url"))
}
return errors.Join(err...)
}

func (f *featureGate) Unmarshal(parser *confmap.Conf) error {
if err := f.validate(parser); err != nil {
return err
}
return parser.Unmarshal(f)
}
85 changes: 85 additions & 0 deletions cmd/mdatagen/internal/loader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@
package internal

import (
"errors"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"go.opentelemetry.io/collector/component"
"go.opentelemetry.io/collector/confmap"
"go.opentelemetry.io/collector/pdata/pcommon"
"go.opentelemetry.io/collector/pdata/pmetric"
)
Expand Down Expand Up @@ -369,6 +372,88 @@ func TestLoadMetadata(t *testing.T) {
}
}

func TestFeatureGateValidate(t *testing.T) {
tests := []struct {
name string
input map[string]interface{}
expectedErr error
}{
{
name: "valid input",
input: map[string]interface{}{
"id": "valid.id",
"stage": "Alpha",
"from_version": "v1.2.3",
"to_version": "v1.3.0",
"reference_url": "http://example.com",
},
expectedErr: nil,
},
{
name: "missing id",
input: map[string]interface{}{
"stage": "Alpha",
},
expectedErr: errors.New("missing required field: `id`"),
},
{
name: "invalid id",
input: map[string]interface{}{
"id": "invalid@id",
"stage": "Alpha",
},
expectedErr: errors.New("invalid character(s) in ID"),
},
{
name: "missing stage",
input: map[string]interface{}{
"id": "valid.id",
},
expectedErr: errors.New("missing required field: `stage`"),
},
{
name: "invalid stage",
input: map[string]interface{}{
"id": "valid.id",
"stage": "InvalidStage",
},
expectedErr: errors.New("invalid stage"),
},
{
name: "invalid from_version",
input: map[string]interface{}{
"id": "valid.id",
"stage": "Alpha",
"from_version": "v1.2.a",
},
expectedErr: errors.New("invalid character(s) in from_version"),
},
{
name: "invalid reference_url",
input: map[string]interface{}{
"id": "valid.id",
"stage": "Alpha",
"reference_url": "invalid-url",
},
expectedErr: errors.New("invalid character(s) in reference_url"),
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
parser := confmap.NewFromStringMap(tt.input)
featureGate := &featureGate{}
err := featureGate.validate(parser)
if tt.expectedErr != nil {
require.Error(t, err)
assert.Contains(t, err.Error(), tt.expectedErr.Error())
} else {
assert.NoError(t, err)
}
})
}
}

func strPtr(s string) *string {
return &s
}
26 changes: 26 additions & 0 deletions cmd/mdatagen/internal/templates/feature_gates.go.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Code generated by mdatagen. DO NOT EDIT.

package {{ .Package }}

import (
"go.opentelemetry.io/collector/featuregate"
)

func init() {
for _, fg := range {{ .FeatureGates }} {
var opts []featuregate.RegisterOption
if fg.FromVersion != "" {
opts = append(opts, featuregate.WithRegisterFromVersion(fg.FromVersion)
}
if fg.Description != "" {
opts = append(opts, featuregate.WithRegisterDescription(fg.Description)
}
if fg.ReferenceURL != "" {
opts = append(opts, featuregate.WithRegisterReferenceURL(fg.ReferenceURL)
}
if fg.ToVersion != "" {
opts = append(opts, featuregate.WithRegisterToVersion("v0.70.0"))
}
_ = featuregate.GlobalRegistry().MustRegister(fg.ID, fg.Stage, opts...)
}
}
17 changes: 17 additions & 0 deletions cmd/mdatagen/metadata-schema.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -175,3 +175,20 @@ telemetry:
# Optional: array of attributes that were defined in the attributes section that are emitted by this metric.
# Note: Only the following attribute types are supported: <string|int|double|bool>
attributes: [string]

#Optional: Gate is an immutable object that is owned by the Registry and represents an individual feature that
# may be enabled or disabled based on the lifecycle state of the feature and CLI flags specified by the user.
feature_gates:
<feature_gate.name>:
#Required: id of the feature gate
id:
#Optional: description of the gate
description:
#Required: current stage at which the feature gate is in the development lifecyle
stage:
#Optional: link to the issue where the gate has been discussed
reference_url:
#Optional: the release version from which the gate has been given the current stage
from_version:
#Optional: the release version till which the gate had the given lifecycle stage
to_version:

0 comments on commit 95dc2e8

Please sign in to comment.