From 68b465a148bf1cf906afecab21069be111e45c23 Mon Sep 17 00:00:00 2001
From: Gemene Narcis <naarcis96@gmail.com>
Date: Wed, 16 Oct 2024 13:42:20 +0300
Subject: [PATCH] [cmd/mdatagen]: Add feature gates support to
 metadata-schema.yaml

---
 cmd/mdatagen/internal/command.go              |  6 +-
 cmd/mdatagen/internal/loader.go               | 78 +++++++++++++++++
 cmd/mdatagen/internal/loader_test.go          | 85 +++++++++++++++++++
 .../internal/templates/feature_gates.go.tmpl  | 26 ++++++
 cmd/mdatagen/metadata-schema.yaml             | 17 ++++
 5 files changed, 211 insertions(+), 1 deletion(-)
 create mode 100644 cmd/mdatagen/internal/templates/feature_gates.go.tmpl

diff --git a/cmd/mdatagen/internal/command.go b/cmd/mdatagen/internal/command.go
index 5abe033a9436..8c7dbd790826 100644
--- a/cmd/mdatagen/internal/command.go
+++ b/cmd/mdatagen/internal/command.go
@@ -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
@@ -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
 	}
diff --git a/cmd/mdatagen/internal/loader.go b/cmd/mdatagen/internal/loader.go
index b51fc86f1414..8ced68ec637e 100644
--- a/cmd/mdatagen/internal/loader.go
+++ b/cmd/mdatagen/internal/loader.go
@@ -9,6 +9,7 @@ import (
 	"fmt"
 	"os/exec"
 	"path/filepath"
+	"regexp"
 	"strings"
 
 	"go.opentelemetry.io/collector/component"
@@ -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) {
@@ -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.
@@ -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
@@ -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) {
@@ -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)
+}
diff --git a/cmd/mdatagen/internal/loader_test.go b/cmd/mdatagen/internal/loader_test.go
index ed404744f75f..c78f4dfa119d 100644
--- a/cmd/mdatagen/internal/loader_test.go
+++ b/cmd/mdatagen/internal/loader_test.go
@@ -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"
 )
@@ -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
 }
diff --git a/cmd/mdatagen/internal/templates/feature_gates.go.tmpl b/cmd/mdatagen/internal/templates/feature_gates.go.tmpl
new file mode 100644
index 000000000000..18d6c2d998dc
--- /dev/null
+++ b/cmd/mdatagen/internal/templates/feature_gates.go.tmpl
@@ -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...)
+    }
+}
\ No newline at end of file
diff --git a/cmd/mdatagen/metadata-schema.yaml b/cmd/mdatagen/metadata-schema.yaml
index afd1f09b62a0..3d2d08ddd700 100644
--- a/cmd/mdatagen/metadata-schema.yaml
+++ b/cmd/mdatagen/metadata-schema.yaml
@@ -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:
\ No newline at end of file