Browse Source

fix(gitlab): restore group variable fallback to wildcard environment_scope and correct precedence (#4965)

* fix(gitlab): fallback to group wildcard variables and fix precedence

Signed-off-by: Aakkash-Suresh <i-aakkash.s@devrev.ai>

* refactor(gitlab): reduce cognitive complexity in fetchProjectVariables

Signed-off-by: Aakkash-Suresh <i-aakkash.s@devrev.ai>

---------

Signed-off-by: Aakkash-Suresh <i-aakkash.s@devrev.ai>
Co-authored-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>
Aakkash Suresh 9 months ago
parent
commit
8e8466b492
2 changed files with 153 additions and 18 deletions
  1. 58 17
      pkg/provider/gitlab/gitlab.go
  2. 95 1
      pkg/provider/gitlab/gitlab_test.go

+ 58 - 17
pkg/provider/gitlab/gitlab.go

@@ -150,6 +150,7 @@ func (g *gitlabBase) GetAllSecrets(_ context.Context, ref esv1.ExternalSecretFin
 
 func (g *gitlabBase) fetchProjectVariables(effectiveEnvironment string, matcher *find.Matcher, secretData map[string][]byte) error {
 	var popts = &gitlab.ListProjectVariablesOptions{PerPage: 100}
+	nonWildcardSet := make(map[string]bool)
 	for projectPage := 1; ; projectPage++ {
 		popts.Page = projectPage
 		projectData, response, err := g.projectVariablesClient.ListVariables(g.store.ProjectID, popts)
@@ -158,18 +159,7 @@ func (g *gitlabBase) fetchProjectVariables(effectiveEnvironment string, matcher
 			return err
 		}
 
-		for _, data := range projectData {
-			matching, key, isWildcard := matchesFilter(effectiveEnvironment, data.EnvironmentScope, data.Key, matcher)
-
-			if !matching {
-				continue
-			}
-			_, exists := secretData[key]
-			if exists && isWildcard {
-				continue
-			}
-			secretData[key] = []byte(data.Value)
-		}
+		processProjectVariables(projectData, effectiveEnvironment, matcher, secretData, nonWildcardSet)
 		if response.CurrentPage >= response.TotalPages {
 			break
 		}
@@ -178,6 +168,28 @@ func (g *gitlabBase) fetchProjectVariables(effectiveEnvironment string, matcher
 	return nil
 }
 
+func processProjectVariables(
+	projectData []*gitlab.ProjectVariable,
+	effectiveEnvironment string,
+	matcher *find.Matcher,
+	secretData map[string][]byte,
+	nonWildcardSet map[string]bool,
+) {
+	for _, data := range projectData {
+		matching, key, isWildcard := matchesFilter(effectiveEnvironment, data.EnvironmentScope, data.Key, matcher)
+		if !matching {
+			continue
+		}
+		if isWildcard && nonWildcardSet[key] {
+			continue
+		}
+		secretData[key] = []byte(data.Value)
+		if !isWildcard {
+			nonWildcardSet[key] = true
+		}
+	}
+}
+
 func (g *gitlabBase) fetchSecretData(effectiveEnvironment string, matcher *find.Matcher) (map[string][]byte, error) {
 	var gopts = &gitlab.ListGroupVariablesOptions{PerPage: 100}
 	secretData := make(map[string][]byte)
@@ -221,7 +233,12 @@ func (g *gitlabBase) setGroupValues(
 ) {
 	for _, data := range groupVars {
 		matching, key, isWildcard := matchesFilter(effectiveEnvironment, data.EnvironmentScope, data.Key, matcher)
-		if !matching && !isWildcard {
+		if !matching {
+			continue
+		}
+		// Check if a more specific variable already exists (project environment > project variable > group environment > group variable)
+		_, exists := secretData[key]
+		if exists && isWildcard {
 			continue
 		}
 		secretData[key] = []byte(data.Value)
@@ -239,6 +256,31 @@ func ExtractTag(tags map[string]string) (string, error) {
 	return environmentScope, nil
 }
 
+func (g *gitlabBase) getGroupVariables(groupID string, ref esv1.ExternalSecretDataRemoteRef, gopts *gitlab.GetGroupVariableOptions) (*gitlab.GroupVariable, *gitlab.Response, error) {
+	groupVar, resp, err := g.groupVariablesClient.GetVariable(groupID, ref.Key, gopts)
+	metrics.ObserveAPICall(constants.ProviderGitLab, constants.CallGitLabGroupGetVariable, err)
+	if err != nil {
+		if resp != nil && resp.StatusCode == http.StatusNotFound && !isEmptyOrWildcard(g.store.Environment) {
+			if gopts == nil {
+				gopts = &gitlab.GetGroupVariableOptions{}
+			}
+			if gopts.Filter == nil {
+				gopts.Filter = &gitlab.VariableFilter{}
+			}
+			gopts.Filter.EnvironmentScope = "*"
+			groupVar, resp, err = g.groupVariablesClient.GetVariable(groupID, ref.Key, gopts)
+			metrics.ObserveAPICall(constants.ProviderGitLab, constants.CallGitLabGroupGetVariable, err)
+			if err != nil || resp == nil {
+				return nil, resp, fmt.Errorf("error getting group variable %s from GitLab: %w", ref.Key, err)
+			}
+		} else {
+			return nil, resp, err
+		}
+	}
+
+	return groupVar, resp, nil
+}
+
 func (g *gitlabBase) GetSecret(_ context.Context, ref esv1.ExternalSecretDataRemoteRef) ([]byte, error) {
 	if utils.IsNil(g.projectVariablesClient) || utils.IsNil(g.groupVariablesClient) {
 		return nil, errors.New(errUninitializedGitlabProvider)
@@ -284,12 +326,11 @@ func (g *gitlabBase) GetSecret(_ context.Context, ref esv1.ExternalSecretDataRem
 			return result, nil
 		}
 
-		groupVar, resp, err := g.groupVariablesClient.GetVariable(groupID, ref.Key, gopts)
-		metrics.ObserveAPICall(constants.ProviderGitLab, constants.CallGitLabGroupGetVariable, err)
-		if resp.StatusCode >= 400 && resp.StatusCode != http.StatusNotFound && err != nil {
+		groupVar, resp, err := g.getGroupVariables(groupID, ref, gopts)
+		if err != nil {
 			return nil, err
 		}
-		if resp.StatusCode < 300 {
+		if resp != nil && resp.StatusCode < 300 {
 			result, _ = extractVariable(ref, groupVar.Value)
 		}
 	}

+ 95 - 1
pkg/provider/gitlab/gitlab_test.go

@@ -245,7 +245,7 @@ func prepareMockProjectVarClient(smtc *secretManagerTestCase) {
 
 func prepareMockGroupVarClient(smtc *secretManagerTestCase) {
 	responses := make([]fakegitlab.APIResponse[[]*gitlab.GroupVariable], 0)
-	if smtc.projectAPIOutput != nil {
+	if smtc.groupAPIOutput != nil {
 		responses = append(responses, fakegitlab.APIResponse[[]*gitlab.GroupVariable]{Output: []*gitlab.GroupVariable{smtc.groupAPIOutput}, Response: smtc.groupAPIResponse, Error: smtc.apiErr})
 	}
 	for _, response := range smtc.groupAPIOutputs {
@@ -430,6 +430,9 @@ func TestGetSecret(t *testing.T) {
 		makeValidSecretManagerTestCaseCustom(onlyWildcardSecret),
 		makeValidSecretManagerTestCaseCustom(groupSecretProjectOverride),
 		makeValidSecretManagerTestCaseCustom(groupWithoutProjectOverride),
+		makeValidSecretManagerTestCaseCustom(setGroupWildcardVariable),
+		makeValidSecretManagerTestCaseCustom(setGroupWildcardVariableWithEnvironment),
+		makeValidSecretManagerTestCaseCustom(setGroupWildcardVariableNotFoundThenFound),
 		makeValidSecretManagerTestCaseCustom(setAPIErr),
 		makeValidSecretManagerTestCaseCustom(setNilMockClient),
 	}
@@ -629,6 +632,55 @@ func TestGetAllSecrets(t *testing.T) {
 		smtc.expectedData = map[string][]byte{testKey: []byte("testValue1"), "testKey2a": []byte("testValue2a"), "testKey2b": []byte("testValue2b"), "testKeyGroup": []byte("groupValue1")}
 		smtc.refFind.Name = makeFindName(findTestPrefix)
 	}
+	setGroupWildcardVariableInGetAllSecrets := func(smtc *secretManagerTestCase) {
+		smtc.groupIDs = []string{groupid}
+		smtc.projectAPIOutput = &gitlab.ProjectVariable{
+			Key:              testKey,
+			Value:            projectvalue,
+			EnvironmentScope: environment,
+		}
+		smtc.groupAPIOutput = &gitlab.GroupVariable{
+			Key:              testKey,
+			Value:            groupvalue,
+			EnvironmentScope: "*",
+		}
+		// Project variable should override group wildcard variable
+		smtc.expectedData = map[string][]byte{testKey: []byte(projectvalue)}
+		smtc.refFind.Name = makeFindName(findTestPrefix)
+	}
+	setGroupWildcardVariableOnlyInGetAllSecrets := func(smtc *secretManagerTestCase) {
+		smtc.groupIDs = []string{groupid}
+		smtc.projectAPIOutput = &gitlab.ProjectVariable{
+			Key:              testKey,
+			Value:            projectvalue,
+			EnvironmentScope: environmentTest, // Different environment
+		}
+		smtc.groupAPIOutput = &gitlab.GroupVariable{
+			Key:              testKey,
+			Value:            groupvalue,
+			EnvironmentScope: "*",
+		}
+		// Group wildcard variable should be used when project variable doesn't match environment
+		smtc.apiInputEnv = environment
+		smtc.expectedData = map[string][]byte{testKey: []byte(groupvalue)}
+		smtc.refFind.Name = makeFindName(findTestPrefix)
+	}
+	setGroupWildcardVariableWithCollision := func(smtc *secretManagerTestCase) {
+		smtc.groupIDs = []string{groupid}
+		smtc.projectAPIOutput = &gitlab.ProjectVariable{
+			Key:              testKey,
+			Value:            projectvalue,
+			EnvironmentScope: "*",
+		}
+		smtc.groupAPIOutput = &gitlab.GroupVariable{
+			Key:              testKey,
+			Value:            groupvalue,
+			EnvironmentScope: "*",
+		}
+		// Project wildcard variable should override group wildcard variable
+		smtc.expectedData = map[string][]byte{testKey: []byte(projectvalue)}
+		smtc.refFind.Name = makeFindName(findTestPrefix)
+	}
 
 	cases := []*secretManagerTestCase{
 		makeValidSecretManagerGetAllTestCaseCustom(setMissingFindRegex),
@@ -642,6 +694,9 @@ func TestGetAllSecrets(t *testing.T) {
 		makeValidSecretManagerGetAllTestCaseCustom(setEnvironmentConstrainedByStore),
 		makeValidSecretManagerGetAllTestCaseCustom(setFilterByEnvironmentWithWildcard),
 		makeValidSecretManagerGetAllTestCaseCustom(setPaginationInGroupAndProjectVars),
+		makeValidSecretManagerGetAllTestCaseCustom(setGroupWildcardVariableInGetAllSecrets),
+		makeValidSecretManagerGetAllTestCaseCustom(setGroupWildcardVariableOnlyInGetAllSecrets),
+		makeValidSecretManagerGetAllTestCaseCustom(setGroupWildcardVariableWithCollision),
 		makeValidSecretManagerGetAllTestCaseCustom(setAPIErr),
 		makeValidSecretManagerGetAllTestCaseCustom(setNilMockClient),
 	}
@@ -897,3 +952,42 @@ func ErrorContains(out error, want string) bool {
 }
 
 type storeModifier func(*esv1.SecretStore) *esv1.SecretStore
+
+func setGroupWildcardVariable(smtc *secretManagerTestCase) {
+	smtc.groupIDs = []string{groupid}
+	smtc.projectAPIResponse.Response.StatusCode = 404
+	smtc.groupAPIOutput.Key = testKey
+	smtc.groupAPIOutput.Value = groupvalue
+	smtc.groupAPIOutput.EnvironmentScope = "*"
+	smtc.expectedSecret = smtc.groupAPIOutput.Value
+}
+
+func setGroupWildcardVariableWithEnvironment(smtc *secretManagerTestCase) {
+	smtc.groupIDs = []string{groupid}
+	smtc.projectAPIResponse.Response.StatusCode = 404
+	smtc.groupAPIOutput.Key = testKey
+	smtc.groupAPIOutput.Value = groupvalue
+	smtc.groupAPIOutput.EnvironmentScope = "*"
+	smtc.apiInputEnv = environment
+	smtc.expectedSecret = smtc.groupAPIOutput.Value
+}
+
+func setGroupWildcardVariableNotFoundThenFound(smtc *secretManagerTestCase) {
+	smtc.groupIDs = []string{groupid}
+	smtc.projectAPIResponse.Response.StatusCode = 404
+
+	// For this test, we'll use a simpler approach - just return the wildcard variable
+	smtc.groupAPIOutput = &gitlab.GroupVariable{
+		Key:              testKey,
+		Value:            groupvalue,
+		EnvironmentScope: "*",
+	}
+	smtc.groupAPIResponse = &gitlab.Response{
+		Response: &http.Response{
+			StatusCode: http.StatusOK,
+		},
+	}
+
+	smtc.apiInputEnv = environment
+	smtc.expectedSecret = groupvalue
+}