diff --git a/gitlab/resource_gitlab_project_variable.go b/gitlab/resource_gitlab_project_variable.go index 5d4b050b5..839864f57 100644 --- a/gitlab/resource_gitlab_project_variable.go +++ b/gitlab/resource_gitlab_project_variable.go @@ -13,6 +13,7 @@ func resourceGitlabProjectVariable() *schema.Resource { Read: resourceGitlabProjectVariableRead, Update: resourceGitlabProjectVariableUpdate, Delete: resourceGitlabProjectVariableDelete, + Exists: resourceGitlabProjectVariableExists, Importer: &schema.ResourceImporter{ State: schema.ImportStatePassthrough, }, @@ -59,6 +60,35 @@ func resourceGitlabProjectVariable() *schema.Resource { } } +func resourceGitlabProjectVariableExists(d *schema.ResourceData, meta interface{}) (bool, error) { + client := meta.(*gitlab.Client) + + project := d.Get("project") + + log.Printf("[DEBUG] list gitlab project variable %s", project) + + options := gitlab.ListProjectVariablesOptions{Page: 1, PerPage: 9999} + projectVariables, _, err := client.ProjectVariables.ListVariables(project, &options) + if err != nil { + return false, err + } + + log.Printf("[DEBUG] gitlab project variables: %s", projectVariables) + + key := d.Get("key") + environmentScope := d.Get("environment_scope") + + for _, projectVariable := range projectVariables { + if projectVariable.Key == key && projectVariable.EnvironmentScope == environmentScope { + log.Printf("[DEBUG] Variable matching key and environment scope exists: %s:%s", key, environmentScope) + return true, nil + } + } + + log.Printf("[DEBUG] Variable matching key and environment scope does NOT exist: %s:%s", key, environmentScope) + return false, nil +} + func resourceGitlabProjectVariableCreate(d *schema.ResourceData, meta interface{}) error { client := meta.(*gitlab.Client) @@ -78,14 +108,14 @@ func resourceGitlabProjectVariableCreate(d *schema.ResourceData, meta interface{ Masked: &masked, EnvironmentScope: &environmentScope, } - log.Printf("[DEBUG] create gitlab project variable %s/%s", project, key) + log.Printf("[DEBUG] create gitlab project variable %s/%s/%s", project, key, environmentScope) _, _, err := client.ProjectVariables.CreateVariable(project, &options) if err != nil { return err } - d.SetId(buildTwoPartID(&project, &key)) + d.SetId(buildThreePartID(&project, &key, &environmentScope)) return resourceGitlabProjectVariableRead(d, meta) } @@ -93,65 +123,147 @@ func resourceGitlabProjectVariableCreate(d *schema.ResourceData, meta interface{ func resourceGitlabProjectVariableRead(d *schema.ResourceData, meta interface{}) error { client := meta.(*gitlab.Client) - project, key, err := parseTwoPartID(d.Id()) + project, key, environmentScope, err := parseThreePartID(d.Id()) if err != nil { return err } log.Printf("[DEBUG] read gitlab project variable %s/%s", project, key) - v, _, err := client.ProjectVariables.GetVariable(project, key) + options := gitlab.ListProjectVariablesOptions{Page: 1, PerPage: 9999} + projectVariables, _, err := client.ProjectVariables.ListVariables(project, &options) if err != nil { return err } - d.Set("key", v.Key) - d.Set("value", v.Value) - d.Set("variable_type", v.VariableType) - d.Set("project", project) - d.Set("protected", v.Protected) - d.Set("masked", v.Masked) - //For now I'm ignoring environment_scope when reading back data. (this can cause configuration drift so it is bad). - //However I'm unable to stop terraform from gratuitously updating this to values that are unacceptable by Gitlab) - //I don't have an enterprise license to properly test this either. - d.Set("environment_scope", v.EnvironmentScope) + log.Printf("[DEBUG] gitlab project variables: %s", projectVariables) + + for _, projectVariable := range projectVariables { + if projectVariable.Key == key && projectVariable.EnvironmentScope == environmentScope { + d.Set("key", projectVariable.Key) + d.Set("value", projectVariable.Value) + d.Set("variable_type", projectVariable.VariableType) + d.Set("project", project) + d.Set("protected", projectVariable.Protected) + d.Set("masked", projectVariable.Masked) + d.Set("environment_scope", projectVariable.EnvironmentScope) + return nil + } + } return nil } func resourceGitlabProjectVariableUpdate(d *schema.ResourceData, meta interface{}) error { client := meta.(*gitlab.Client) - - project := d.Get("project").(string) + project := d.Get("project") key := d.Get("key").(string) - value := d.Get("value").(string) - variableType := stringToVariableType(d.Get("variable_type").(string)) - protected := d.Get("protected").(bool) - masked := d.Get("masked").(bool) - environmentScope := d.Get("environment_scope").(string) + environmentScopeOfProjectVariableBeingUpdated := d.Get("environment_scope") - options := &gitlab.UpdateProjectVariableOptions{ - Value: &value, - VariableType: variableType, - Protected: &protected, - Masked: &masked, - EnvironmentScope: &environmentScope, + projectVariablesWithSameKeyAsTheOneBeingUpdated, fetchErr := projectVariablesMatchingKey(client, project, key) + if fetchErr != nil { + return fetchErr } - log.Printf("[DEBUG] update gitlab project variable %s/%s", project, key) - _, _, err := client.ProjectVariables.UpdateVariable(project, key, options) - if err != nil { - return err + deleteErr := deleteProjectVariables(projectVariablesWithSameKeyAsTheOneBeingUpdated, project, client) + if deleteErr != nil { + return deleteErr } + for _, projectVariable := range projectVariablesWithSameKeyAsTheOneBeingUpdated { + environmentScope := projectVariable.EnvironmentScope + var value string + var variableType gitlab.VariableTypeValue + var protected bool + var masked bool + if environmentScope == environmentScopeOfProjectVariableBeingUpdated { + value = d.Get("value").(string) + variableType = *stringToVariableType(d.Get("variable_type").(string)) + protected = d.Get("protected").(bool) + masked = d.Get("masked").(bool) + } else { + value = projectVariable.Value + variableType = projectVariable.VariableType + protected = projectVariable.Protected + masked = projectVariable.Masked + } + options := gitlab.CreateProjectVariableOptions{ + Key: &key, + Value: &value, + VariableType: &variableType, + Protected: &protected, + Masked: &masked, + EnvironmentScope: &environmentScope, + } + log.Printf("[DEBUG] create gitlab project variable %s/%s/%s", project, key, environmentScope) + _, _, err := client.ProjectVariables.CreateVariable(project, &options) + if err != nil { + return err + } + } return resourceGitlabProjectVariableRead(d, meta) } func resourceGitlabProjectVariableDelete(d *schema.ResourceData, meta interface{}) error { client := meta.(*gitlab.Client) - project := d.Get("project").(string) + project := d.Get("project") key := d.Get("key").(string) - log.Printf("[DEBUG] Delete gitlab project variable %s/%s", project, key) + environmentScopeOfProjectVariableBeingDeleted := d.Get("environment_scope") + + projectVariablesWithSameKeyAsTheOneBeingDeleted, fetchErr := projectVariablesMatchingKey(client, project, key) + if fetchErr != nil { + return fetchErr + } + + deleteErr := deleteProjectVariables(projectVariablesWithSameKeyAsTheOneBeingDeleted, project, client) + if deleteErr != nil { + return deleteErr + } + + for _, projectVariable := range projectVariablesWithSameKeyAsTheOneBeingDeleted { + if projectVariable.EnvironmentScope == environmentScopeOfProjectVariableBeingDeleted { + continue // Don't re-create project variable for the environment scope that is marked for deletion + } + options := gitlab.CreateProjectVariableOptions{ + Key: &projectVariable.Key, + Value: &projectVariable.Value, + VariableType: &projectVariable.VariableType, + Protected: &projectVariable.Protected, + Masked: &projectVariable.Masked, + EnvironmentScope: &projectVariable.EnvironmentScope, + } + log.Printf("[DEBUG] create gitlab project variable %s/%s/%s", project, key, projectVariable.EnvironmentScope) + _, _, err := client.ProjectVariables.CreateVariable(project, &options) + if err != nil { + return err + } + } + return nil +} + +func deleteProjectVariables(projectVariables []*gitlab.ProjectVariable, project interface{}, client *gitlab.Client) error { + for _, projectVariable := range projectVariables { + log.Printf("[DEBUG] Delete gitlab project variable %s/%s", project, projectVariable.Key) + _, err := client.ProjectVariables.RemoveVariable(project, projectVariable.Key) + if err != nil { + return err + } + } + return nil +} + +func projectVariablesMatchingKey(client *gitlab.Client, project interface{}, key string) ([]*gitlab.ProjectVariable, error) { + options := gitlab.ListProjectVariablesOptions{Page: 1, PerPage: 9999} + allProjectVariables, _, err := client.ProjectVariables.ListVariables(project, &options) + if err != nil { + return nil, err + } + + var projectVariablesWithSameKey []*gitlab.ProjectVariable - _, err := client.ProjectVariables.RemoveVariable(project, key) - return err + for _, projectVariable := range allProjectVariables { + if projectVariable.Key == key { + projectVariablesWithSameKey = append(projectVariablesWithSameKey, projectVariable) + } + } + return projectVariablesWithSameKey, nil } diff --git a/gitlab/util.go b/gitlab/util.go index 9c493635f..f462b2527 100644 --- a/gitlab/util.go +++ b/gitlab/util.go @@ -159,6 +159,21 @@ func buildTwoPartID(a, b *string) string { return fmt.Sprintf("%s:%s", *a, *b) } +// format the strings into an id `a:b:c` +func buildThreePartID(a, b *string, c *string) string { + return fmt.Sprintf("%s:%s:%s", *a, *b, *c) +} + +// return the pieces of id `a:b:c` as a, b, c +func parseThreePartID(id string) (string, string, string, error) { + parts := strings.SplitN(id, ":", 3) + if len(parts) != 3 { + return "", "", "", fmt.Errorf("Unexpected ID format (%q). Expected project:key:env", id) + } + + return parts[0], parts[1], parts[2], nil +} + var accessLevelID = map[string]gitlab.AccessLevelValue{ "no one": gitlab.NoPermissions, "guest": gitlab.GuestPermissions,