|  | 
|  | 1 | +package gitlab | 
|  | 2 | + | 
|  | 3 | +import ( | 
|  | 4 | +	"context" | 
|  | 5 | +	"encoding/base64" | 
|  | 6 | +	"fmt" | 
|  | 7 | +	"log" | 
|  | 8 | +	"strings" | 
|  | 9 | + | 
|  | 10 | +	"github.com/hashicorp/terraform-plugin-sdk/v2/diag" | 
|  | 11 | +	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" | 
|  | 12 | +	gitlab "github.com/xanzy/go-gitlab" | 
|  | 13 | +) | 
|  | 14 | + | 
|  | 15 | +const encoding = "base64" | 
|  | 16 | + | 
|  | 17 | +func resourceGitLabRepositoryFile() *schema.Resource { | 
|  | 18 | +	return &schema.Resource{ | 
|  | 19 | +		CreateContext: resourceGitlabRepositoryFileCreate, | 
|  | 20 | +		ReadContext:   resourceGitlabRepositoryFileRead, | 
|  | 21 | +		UpdateContext: resourceGitlabRepositoryFileUpdate, | 
|  | 22 | +		DeleteContext: resourceGitlabRepositoryFileDelete, | 
|  | 23 | +		Importer: &schema.ResourceImporter{ | 
|  | 24 | +			StateContext: schema.ImportStatePassthroughContext, | 
|  | 25 | +		}, | 
|  | 26 | + | 
|  | 27 | +		// the schema matches https://docs.gitlab.com/ee/api/repository_files.html#create-new-file-in-repository | 
|  | 28 | +		// However, we don't support the `encoding` parameter as it seems to be broken. | 
|  | 29 | +		// Only a value of `base64` is supported, all others, including the documented default `text`, lead to | 
|  | 30 | +		// a `400 {error: encoding does not have a valid value}` error. | 
|  | 31 | +		Schema: map[string]*schema.Schema{ | 
|  | 32 | +			"project": { | 
|  | 33 | +				Type:     schema.TypeString, | 
|  | 34 | +				Required: true, | 
|  | 35 | +				ForceNew: true, | 
|  | 36 | +			}, | 
|  | 37 | +			"file_path": { | 
|  | 38 | +				Type:     schema.TypeString, | 
|  | 39 | +				Required: true, | 
|  | 40 | +				ForceNew: true, | 
|  | 41 | +			}, | 
|  | 42 | +			"branch": { | 
|  | 43 | +				Type:     schema.TypeString, | 
|  | 44 | +				Required: true, | 
|  | 45 | +				ForceNew: true, | 
|  | 46 | +			}, | 
|  | 47 | +			"start_branch": { | 
|  | 48 | +				Type:     schema.TypeString, | 
|  | 49 | +				Optional: true, | 
|  | 50 | +			}, | 
|  | 51 | +			"author_email": { | 
|  | 52 | +				Type:     schema.TypeString, | 
|  | 53 | +				Optional: true, | 
|  | 54 | +			}, | 
|  | 55 | +			"author_name": { | 
|  | 56 | +				Type:     schema.TypeString, | 
|  | 57 | +				Optional: true, | 
|  | 58 | +			}, | 
|  | 59 | +			"content": { | 
|  | 60 | +				Type:         schema.TypeString, | 
|  | 61 | +				Required:     true, | 
|  | 62 | +				ValidateFunc: validateBase64Content, | 
|  | 63 | +			}, | 
|  | 64 | +			"commit_message": { | 
|  | 65 | +				Type:     schema.TypeString, | 
|  | 66 | +				Required: true, | 
|  | 67 | +			}, | 
|  | 68 | +			"encoding": { | 
|  | 69 | +				Type:     schema.TypeString, | 
|  | 70 | +				Computed: true, | 
|  | 71 | +			}, | 
|  | 72 | +		}, | 
|  | 73 | +	} | 
|  | 74 | +} | 
|  | 75 | + | 
|  | 76 | +func resourceGitlabRepositoryFileCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { | 
|  | 77 | +	client := meta.(*gitlab.Client) | 
|  | 78 | +	project := d.Get("project").(string) | 
|  | 79 | +	filePath := d.Get("file_path").(string) | 
|  | 80 | + | 
|  | 81 | +	options := &gitlab.CreateFileOptions{ | 
|  | 82 | +		Branch:        gitlab.String(d.Get("branch").(string)), | 
|  | 83 | +		Encoding:      gitlab.String(encoding), | 
|  | 84 | +		AuthorEmail:   gitlab.String(d.Get("author_email").(string)), | 
|  | 85 | +		AuthorName:    gitlab.String(d.Get("author_name").(string)), | 
|  | 86 | +		Content:       gitlab.String(d.Get("content").(string)), | 
|  | 87 | +		CommitMessage: gitlab.String(d.Get("commit_message").(string)), | 
|  | 88 | +	} | 
|  | 89 | +	if startBranch, ok := d.GetOk("start_branch"); ok { | 
|  | 90 | +		options.StartBranch = gitlab.String(startBranch.(string)) | 
|  | 91 | +	} | 
|  | 92 | + | 
|  | 93 | +	repositoryFile, _, err := client.RepositoryFiles.CreateFile(project, filePath, options, gitlab.WithContext(ctx)) | 
|  | 94 | +	if err != nil { | 
|  | 95 | +		return diag.FromErr(err) | 
|  | 96 | +	} | 
|  | 97 | + | 
|  | 98 | +	d.SetId(resourceGitLabRepositoryFileBuildId(project, repositoryFile.Branch, repositoryFile.FilePath)) | 
|  | 99 | +	return resourceGitlabRepositoryFileRead(ctx, d, meta) | 
|  | 100 | +} | 
|  | 101 | + | 
|  | 102 | +func resourceGitlabRepositoryFileRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { | 
|  | 103 | +	client := meta.(*gitlab.Client) | 
|  | 104 | +	project, branch, filePath, err := resourceGitLabRepositoryFileParseId(d.Id()) | 
|  | 105 | +	if err != nil { | 
|  | 106 | +		return diag.FromErr(err) | 
|  | 107 | +	} | 
|  | 108 | + | 
|  | 109 | +	options := &gitlab.GetFileOptions{ | 
|  | 110 | +		Ref: gitlab.String(branch), | 
|  | 111 | +	} | 
|  | 112 | + | 
|  | 113 | +	repositoryFile, _, err := client.RepositoryFiles.GetFile(project, filePath, options, gitlab.WithContext(ctx)) | 
|  | 114 | +	if err != nil { | 
|  | 115 | +		if strings.Contains(err.Error(), "404 File Not Found") { | 
|  | 116 | +			log.Printf("[WARN] file %s not found, removing from state", filePath) | 
|  | 117 | +			d.SetId("") | 
|  | 118 | +			return nil | 
|  | 119 | +		} | 
|  | 120 | +		return diag.FromErr(err) | 
|  | 121 | +	} | 
|  | 122 | + | 
|  | 123 | +	d.SetId(resourceGitLabRepositoryFileBuildId(project, branch, repositoryFile.FilePath)) | 
|  | 124 | +	d.Set("project", project) | 
|  | 125 | +	d.Set("file_path", repositoryFile.FilePath) | 
|  | 126 | +	d.Set("branch", repositoryFile.Ref) | 
|  | 127 | +	d.Set("encoding", repositoryFile.Encoding) | 
|  | 128 | +	d.Set("content", repositoryFile.Content) | 
|  | 129 | + | 
|  | 130 | +	return nil | 
|  | 131 | +} | 
|  | 132 | + | 
|  | 133 | +func resourceGitlabRepositoryFileUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { | 
|  | 134 | +	client := meta.(*gitlab.Client) | 
|  | 135 | +	project, branch, filePath, err := resourceGitLabRepositoryFileParseId(d.Id()) | 
|  | 136 | +	if err != nil { | 
|  | 137 | +		return diag.FromErr(err) | 
|  | 138 | +	} | 
|  | 139 | + | 
|  | 140 | +	readOptions := &gitlab.GetFileOptions{ | 
|  | 141 | +		Ref: gitlab.String(branch), | 
|  | 142 | +	} | 
|  | 143 | + | 
|  | 144 | +	existingRepositoryFile, _, err := client.RepositoryFiles.GetFile(project, filePath, readOptions, gitlab.WithContext(ctx)) | 
|  | 145 | +	if err != nil { | 
|  | 146 | +		return diag.FromErr(err) | 
|  | 147 | +	} | 
|  | 148 | + | 
|  | 149 | +	options := &gitlab.UpdateFileOptions{ | 
|  | 150 | +		Branch:        gitlab.String(branch), | 
|  | 151 | +		Encoding:      gitlab.String(encoding), | 
|  | 152 | +		AuthorEmail:   gitlab.String(d.Get("author_email").(string)), | 
|  | 153 | +		AuthorName:    gitlab.String(d.Get("author_name").(string)), | 
|  | 154 | +		Content:       gitlab.String(d.Get("content").(string)), | 
|  | 155 | +		CommitMessage: gitlab.String(d.Get("commit_message").(string)), | 
|  | 156 | +		LastCommitID:  gitlab.String(existingRepositoryFile.LastCommitID), | 
|  | 157 | +	} | 
|  | 158 | +	if startBranch, ok := d.GetOk("start_branch"); ok { | 
|  | 159 | +		options.StartBranch = gitlab.String(startBranch.(string)) | 
|  | 160 | +	} | 
|  | 161 | + | 
|  | 162 | +	_, _, err = client.RepositoryFiles.UpdateFile(project, filePath, options, gitlab.WithContext(ctx)) | 
|  | 163 | +	if err != nil { | 
|  | 164 | +		return diag.FromErr(err) | 
|  | 165 | +	} | 
|  | 166 | + | 
|  | 167 | +	return resourceGitlabRepositoryFileRead(ctx, d, meta) | 
|  | 168 | +} | 
|  | 169 | + | 
|  | 170 | +func resourceGitlabRepositoryFileDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { | 
|  | 171 | +	client := meta.(*gitlab.Client) | 
|  | 172 | +	project, branch, filePath, err := resourceGitLabRepositoryFileParseId(d.Id()) | 
|  | 173 | +	if err != nil { | 
|  | 174 | +		return diag.FromErr(err) | 
|  | 175 | +	} | 
|  | 176 | + | 
|  | 177 | +	readOptions := &gitlab.GetFileOptions{ | 
|  | 178 | +		Ref: gitlab.String(branch), | 
|  | 179 | +	} | 
|  | 180 | + | 
|  | 181 | +	existingRepositoryFile, _, err := client.RepositoryFiles.GetFile(project, filePath, readOptions, gitlab.WithContext(ctx)) | 
|  | 182 | +	if err != nil { | 
|  | 183 | +		return diag.FromErr(err) | 
|  | 184 | +	} | 
|  | 185 | + | 
|  | 186 | +	options := &gitlab.DeleteFileOptions{ | 
|  | 187 | +		Branch:        gitlab.String(d.Get("branch").(string)), | 
|  | 188 | +		AuthorEmail:   gitlab.String(d.Get("author_email").(string)), | 
|  | 189 | +		AuthorName:    gitlab.String(d.Get("author_name").(string)), | 
|  | 190 | +		CommitMessage: gitlab.String(fmt.Sprintf("[DELETE]: %s", d.Get("commit_message").(string))), | 
|  | 191 | +		LastCommitID:  gitlab.String(existingRepositoryFile.LastCommitID), | 
|  | 192 | +	} | 
|  | 193 | + | 
|  | 194 | +	resp, err := client.RepositoryFiles.DeleteFile(project, filePath, options) | 
|  | 195 | +	if err != nil { | 
|  | 196 | +		return diag.Errorf("%s failed to delete repository file: (%s) %v", d.Id(), resp.Status, err) | 
|  | 197 | +	} | 
|  | 198 | + | 
|  | 199 | +	return nil | 
|  | 200 | +} | 
|  | 201 | + | 
|  | 202 | +func validateBase64Content(v interface{}, k string) (we []string, errors []error) { | 
|  | 203 | +	content := v.(string) | 
|  | 204 | +	if _, err := base64.StdEncoding.DecodeString(content); err != nil { | 
|  | 205 | +		errors = append(errors, fmt.Errorf("given repository file content '%s' is not base64 encoded, but must be", content)) | 
|  | 206 | +	} | 
|  | 207 | +	return | 
|  | 208 | +} | 
|  | 209 | + | 
|  | 210 | +func resourceGitLabRepositoryFileParseId(id string) (string, string, string, error) { | 
|  | 211 | +	parts := strings.SplitN(id, ":", 3) | 
|  | 212 | +	if len(parts) != 3 { | 
|  | 213 | +		return "", "", "", fmt.Errorf("Unexpected ID format (%q). Expected project:branch:repository_file_path", id) | 
|  | 214 | +	} | 
|  | 215 | + | 
|  | 216 | +	return parts[0], parts[1], parts[2], nil | 
|  | 217 | +} | 
|  | 218 | + | 
|  | 219 | +func resourceGitLabRepositoryFileBuildId(project string, branch string, filePath string) string { | 
|  | 220 | +	return fmt.Sprintf("%s:%s:%s", project, branch, filePath) | 
|  | 221 | +} | 
0 commit comments