Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
The diff you're trying to view is too large. We only load the first 3000 changed files.
128 changes: 88 additions & 40 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,35 +1,49 @@
# terraform-provider-githubfile

[![Build status](https://github.com/form3tech-oss/terraform-provider-githubfile/actions/workflows/ci.yaml/badge.svg)](https://github.com/form3tech-oss/terraform-provider-githubfile/actions)
[![Build status](https://github.com/form3tech-oss/terraform-provider-githubfile/actions/workflows/ci.yaml/badge.svg?branch=master)](https://github.com/form3tech-oss/terraform-provider-githubfile/actions)
[![License](https://img.shields.io/github/license/form3tech-oss/terraform-provider-githubfile)](LICENSE)

A Terraform provider for managing files in GitHub repositories.

## Use-cases
## Use Cases

A few possible use-cases for `terraform-provider-githubfile` are:
A few possible use cases for `terraform-provider-githubfile` are:

* Adding a `LICENSE` file to a number of repositories.
* Making sure repositories across an organisation have consistent issue/pull request templates.
* Configuring a tool such as [`golangci-lint`](https://github.com/golangci/golangci-lint) or [`pre-commit`](https://pre-commit.com/) uniformly across a number of repositories
* Configuring a tool such as [`golangci-lint`](https://github.com/golangci/golangci-lint) or [`pre-commit`](https://pre-commit.com/) uniformly across a number of repositories.

## Installation

Download the relevant binary from [releases](https://github.com/form3tech-oss/terraform-provider-github-file/releases) and copy it to `$HOME/.terraform.d/plugins/`.
### Manual Installation

Download the relevant binary from [releases](https://github.com/form3tech-oss/terraform-provider-githubfile/releases) and copy it to `$HOME/.terraform.d/plugins/`.

## Configuration

The following provider block variables are available for configuration:

| Name | Description |
| ---- | ----------- |
| `commit_message_prefix` | An optional prefix to be added to all commits generated as a result of manipulating files. |
| `github_email` | The email address to use for commit messages.<br>If a GPG key is provided, this must match the one which the key corresponds to. |
| `github_token` | A GitHub authorisation token with `repo` permissions and having `admin` access to the target repositories. |
| `github_username` | The username to use for commit messages. |
| `gpg_passphrase` | The passphrase associated with the provided `gpg_secret_key` (see below). |
| `gpg_secret_key` | The GPG secret key to be use for commit signing.<br>If left empty, commits will not be signed. |
| Name | Required | Environment Variable | Description |
| ---- | :------: | -------------------- | ----------- |
| `github_token` | **Yes** | `GITHUB_TOKEN` | A GitHub authorisation token with permissions to manage files in the target repositories. |
| `github_email` | **Yes** | `GITHUB_EMAIL` | The email address to use for commit messages. If a GPG key is provided, this must match the one which the key corresponds to. |
| `github_username` | **Yes** | `GITHUB_USERNAME` | The username to use for commit messages. |
| `commit_message_prefix` | No | `COMMIT_MESSAGE_PREFIX` | An optional prefix to be added to all commits generated as a result of manipulating files. |
| `gpg_secret_key` | No | `GPG_SECRET_KEY` | The GPG secret key to use for commit signing. Accepts raw or base64-encoded values. If left empty, commits will not be signed. |
| `gpg_passphrase` | No | `GPG_PASSPHRASE` | The passphrase associated with the provided `gpg_secret_key`. |

Each variable can be set either in the provider block or via the corresponding environment variable. Provider block values take precedence over environment variables.

### Example

Alternatively, these values can be read from environment variables.
```hcl
provider "githubfile" {
github_token = var.github_token
github_email = "ci-bot@example.com"
github_username = "ci-bot"
commit_message_prefix = "[terraform]"
}
```

## Resources

Expand All @@ -39,44 +53,78 @@ The `githubfile_file` resource represents a file in a given branch of a GitHub r

#### Attributes

| Name | Description |
| ---- | ----------- |
| `repository_owner` | The owner of the repository. |
| `repository_name` | The name of the repository. |
| `branch` | The branch in which to create/update the file<br>Leaving this empty will cause the file to be created/updated in the default branch. |
| `path` | The path to the file being created/updated. |
| `contents` | The contents of the file. |
| Name | Type | Required | Description |
| ---- | :--: | :------: | ----------- |
| `id` | String | Computed | The ID of the file resource (format: `owner/repo:branch:path`). |
| `repository_owner` | String | **Yes** | The owner of the repository. Changing this forces a new resource. |
| `repository_name` | String | **Yes** | The name of the repository. Changing this forces a new resource. |
| `branch` | String | **Yes** | The branch in which to create/update the file. Changing this forces a new resource. |
| `path` | String | **Yes** | The path to the file being created/updated. Changing this forces a new resource. |
| `contents` | String | **Yes** | The contents of the file. |

> **Note:** When a managed file is in an archived repository, the provider will gracefully skip deletion and simply remove the resource from state.

#### Example

```hcl
resource "githubfile_file" "form3tech_oss_terraform_provider_githubfile_issue_template" {
repository_owner = "form3tech-oss"
repository_name = "terraform-provider-githubfile"
branch = ""
path = ".github/ISSUE_TEMPLATE.md"
contents = <<EOF
# Issue Type
resource "githubfile_file" "issue_template" {
repository_owner = "form3tech-oss"
repository_name = "terraform-provider-githubfile"
branch = "main"
path = ".github/ISSUE_TEMPLATE.md"
contents = <<-EOF
# Issue Type

- [ ] Bug report.
- [ ] Suggestion.
- [ ] Bug report.
- [ ] Suggestion.

# Description
# Description

<!-- Please provide a description of the issue. -->
EOF
<!-- Please provide a description of the issue. -->
EOF
}
```

Creating the resource above will result in the `.github/ISSUE_TEMPLATE.md` file being created/updated on the default branch of the `form3tech-oss/terraform-provider-githubfile` repository with the following contents:
Creating the resource above will result in the `.github/ISSUE_TEMPLATE.md` file being created/updated on the `main` branch of the `form3tech-oss/terraform-provider-githubfile` repository.

#### Import

Existing files can be imported into Terraform state using the following ID format:

```
owner/repo:branch:path
```

For example:

```bash
terraform import githubfile_file.issue_template form3tech-oss/terraform-provider-githubfile:main:.github/ISSUE_TEMPLATE.md
```

## Development

```markdown
# Issue Type
### Requirements

- [ ] Bug report.
- [ ] Suggestion.
* [Go](https://golang.org/dl/) (see `go.mod` for the required version)
* A GitHub token with `repo` permissions (set as `GITHUB_TOKEN`)

# Description
### Building

<!-- Please provide a description of the issue. -->
```bash
make build
```

### Running Tests

Acceptance tests require a valid GitHub token and run against the `form3tech-oss/terraform-provider-githubfile` repository:

```bash
export GITHUB_TOKEN="your-token"
export GITHUB_EMAIL="your-email@example.com"
export GITHUB_USERNAME="your-username"
make test
```

## License

This project is licensed under the [Apache License 2.0](LICENSE).
196 changes: 119 additions & 77 deletions githubfile/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,26 +15,20 @@
package githubfile

import (
"context"
"encoding/base64"
"fmt"
"os"

"github.com/google/go-github/v54/github"
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
tpg "github.com/integrations/terraform-provider-github/v5/github"
"strings"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/provider"
"github.com/hashicorp/terraform-plugin-framework/provider/schema"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/types"
"golang.org/x/oauth2"
)

const (
resourceFileName = "githubfile_file"
)

const (
commitMessagePrefixKey = "commit_message_prefix"
githubEmailKey = "github_email"
githubTokenKey = "github_token"
githubUsernameKey = "github_username"
gpgPassphraseKey = "gpg_passphrase"
gpgSecretKeyKey = "gpg_secret_key"
)
var _ provider.Provider = &githubfileProvider{}

type providerConfiguration struct {
commitMessagePrefix string
Expand All @@ -45,83 +39,131 @@ type providerConfiguration struct {
gpgSecretKey string
}

func Provider() *schema.Provider {
return &schema.Provider{
ConfigureFunc: func(d *schema.ResourceData) (interface{}, error) {
c := tpg.Config{
Token: d.Get(githubTokenKey).(string),
BaseURL: "https://api.github.com/",
}

gc, err := c.NewRESTClient(c.AuthenticatedHTTPClient())
if err != nil {
return nil, fmt.Errorf("failed to create GitHub Client: %v", err)
}

// Support reading a base64-encoded GPG secret key.
sk := d.Get(gpgSecretKeyKey).(string)
if v, err := base64.StdEncoding.DecodeString(sk); err == nil {
sk = string(v)
}
return &providerConfiguration{
commitMessagePrefix: d.Get(commitMessagePrefixKey).(string),
githubClient: gc,
githubEmail: d.Get(githubEmailKey).(string),
githubUsername: d.Get(githubUsernameKey).(string),
gpgSecretKey: sk,
gpgPassphrase: d.Get(gpgPassphraseKey).(string),
}, nil
},
ResourcesMap: map[string]*schema.Resource{
resourceFileName: resourceFile(),
},
Schema: map[string]*schema.Schema{
commitMessagePrefixKey: {
Type: schema.TypeString,
DefaultFunc: defaultFuncForKey(commitMessagePrefixKey),
type githubfileProvider struct{}

type githubfileProviderModel struct {
CommitMessagePrefix types.String `tfsdk:"commit_message_prefix"`
GithubEmail types.String `tfsdk:"github_email"`
GithubToken types.String `tfsdk:"github_token"`
GithubUsername types.String `tfsdk:"github_username"`
GpgPassphrase types.String `tfsdk:"gpg_passphrase"`
GpgSecretKey types.String `tfsdk:"gpg_secret_key"`
}

// New returns a new instance of the githubfile provider.
func New() provider.Provider {
return &githubfileProvider{}
}

func (p *githubfileProvider) Metadata(_ context.Context, _ provider.MetadataRequest, resp *provider.MetadataResponse) {
resp.TypeName = "githubfile"
}

func (p *githubfileProvider) Schema(_ context.Context, _ provider.SchemaRequest, resp *provider.SchemaResponse) {
resp.Schema = schema.Schema{
Attributes: map[string]schema.Attribute{
"commit_message_prefix": schema.StringAttribute{
Optional: true,
Sensitive: false,
Description: "An optional prefix to be added to all commits created as a result of manipulating files.",
Description: "An optional prefix to be added to all commits created as a result of manipulating files. Can also be set via the COMMIT_MESSAGE_PREFIX environment variable.",
},
githubEmailKey: {
Type: schema.TypeString,
DefaultFunc: defaultFuncForKey(githubEmailKey),
Required: true,
"github_email": schema.StringAttribute{
Optional: true,
Sensitive: true,
Description: "The email address to use for commit messages. If a GPG key is provided, this must match the one which the key corresponds to.",
Description: "The email address to use for commit messages. If a GPG key is provided, this must match the one which the key corresponds to. Can also be set via the GITHUB_EMAIL environment variable.",
},
githubTokenKey: {
Type: schema.TypeString,
DefaultFunc: defaultFuncForKey(githubTokenKey),
Required: true,
"github_token": schema.StringAttribute{
Optional: true,
Sensitive: true,
Description: "A GitHub authorisation token with permissions to manage CRUD files in the target repositories.",
Description: "A GitHub authorisation token with permissions to manage CRUD files in the target repositories. Can also be set via the GITHUB_TOKEN environment variable.",
},
githubUsernameKey: {
Type: schema.TypeString,
DefaultFunc: defaultFuncForKey(githubUsernameKey),
Required: true,
"github_username": schema.StringAttribute{
Optional: true,
Sensitive: true,
Description: "The username to use for commit messages.",
Description: "The username to use for commit messages. Can also be set via the GITHUB_USERNAME environment variable.",
},
gpgPassphraseKey: {
Type: schema.TypeString,
DefaultFunc: defaultFuncForKey(gpgPassphraseKey),
"gpg_passphrase": schema.StringAttribute{
Optional: true,
Sensitive: true,
Description: fmt.Sprintf("The passphrase associated with the provided %q.", gpgSecretKeyKey),
Description: "The passphrase associated with the provided \"gpg_secret_key\". Can also be set via the GPG_PASSPHRASE environment variable.",
},
gpgSecretKeyKey: {
Type: schema.TypeString,
DefaultFunc: defaultFuncForKey(gpgSecretKeyKey),
"gpg_secret_key": schema.StringAttribute{
Optional: true,
Sensitive: true,
Description: "The GPG secret key to be use for commit signing.",
Description: "The GPG secret key to be use for commit signing. Can also be set via the GPG_SECRET_KEY environment variable.",
},
},
}
}

func defaultFuncForKey(v string) schema.SchemaDefaultFunc {
return schema.EnvDefaultFunc(strings.ToUpper(v), "")
func (p *githubfileProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
var config githubfileProviderModel
resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
if resp.Diagnostics.HasError() {
return
}

token := stringValueOrEnv(config.GithubToken, "GITHUB_TOKEN")
if token == "" {
resp.Diagnostics.AddError(
"Missing GitHub Token",
"github_token must be configured or the GITHUB_TOKEN environment variable must be set.",
)
return
}

email := stringValueOrEnv(config.GithubEmail, "GITHUB_EMAIL")
if email == "" {
resp.Diagnostics.AddError(
"Missing GitHub Email",
"github_email must be configured or the GITHUB_EMAIL environment variable must be set.",
)
return
}

username := stringValueOrEnv(config.GithubUsername, "GITHUB_USERNAME")
if username == "" {
resp.Diagnostics.AddError(
"Missing GitHub Username",
"github_username must be configured or the GITHUB_USERNAME environment variable must be set.",
)
return
}

ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token})
tc := oauth2.NewClient(ctx, ts)
gc := github.NewClient(tc)

sk := stringValueOrEnv(config.GpgSecretKey, "GPG_SECRET_KEY")
if v, err := base64.StdEncoding.DecodeString(sk); err == nil {
sk = string(v)
}

providerConfig := &providerConfiguration{
commitMessagePrefix: stringValueOrEnv(config.CommitMessagePrefix, "COMMIT_MESSAGE_PREFIX"),
githubClient: gc,
githubEmail: email,
githubUsername: username,
gpgSecretKey: sk,
gpgPassphrase: stringValueOrEnv(config.GpgPassphrase, "GPG_PASSPHRASE"),
}

resp.DataSourceData = providerConfig
resp.ResourceData = providerConfig
}

func (p *githubfileProvider) Resources(_ context.Context) []func() resource.Resource {
return []func() resource.Resource{
NewFileResource,
}
}

func (p *githubfileProvider) DataSources(_ context.Context) []func() datasource.DataSource {
return nil
}

func stringValueOrEnv(v types.String, envKey string) string {
if !v.IsNull() && !v.IsUnknown() {
return v.ValueString()
}
return os.Getenv(envKey)
}
Loading
Loading