mirror of
https://github.com/qdm12/ddns-updater.git
synced 2026-04-05 08:54:09 -04:00
docs: add contributing document with example provider
- add example provider in code and docs markdown file - merge contributing guides together - add contributing section on adding a new provider
This commit is contained in:
81
.github/CONTRIBUTING.md
vendored
81
.github/CONTRIBUTING.md
vendored
@@ -1,17 +1,92 @@
|
||||
# Contributing
|
||||
|
||||
Contributions are [released](https://help.github.com/articles/github-terms-of-service/#6-contributions-under-repository-license) to the public under the [open source license of this project](../LICENSE).
|
||||
## Table of content
|
||||
|
||||
1. [Submitting a pull request](#submitting-a-pull-request)
|
||||
1. [Development setup](#development-setup)
|
||||
1. [Commands available](#commands-available)
|
||||
1. [Add a new DNS provider](#add-a-new-dns-provider)
|
||||
1. [License](#license)
|
||||
|
||||
## Submitting a pull request
|
||||
|
||||
1. [Fork](https://github.com/qdm12/ddns-updater/fork) and clone the repository
|
||||
1. Create a new branch `git checkout -b my-branch-name`
|
||||
1. Modify the code
|
||||
1. Ensure the docker build succeeds `docker build .`
|
||||
1. Commit your modifications
|
||||
1. Push to your fork and [submit a pull request](https://github.com/qdm12/ddns-updater/compare)
|
||||
|
||||
## Resources
|
||||
Additional resources:
|
||||
|
||||
- [Using Pull Requests](https://help.github.com/articles/about-pull-requests/)
|
||||
- [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/)
|
||||
|
||||
## Development setup
|
||||
|
||||
### Using VSCode and Docker
|
||||
|
||||
That should be easier and better than a local setup, although it might use more memory if you're not on Linux.
|
||||
|
||||
1. Install [Docker](https://docs.docker.com/install/)
|
||||
- On Windows, share a drive with Docker Desktop and have the project on that partition
|
||||
- On OSX, share your project directory with Docker Desktop
|
||||
1. With [Visual Studio Code](https://code.visualstudio.com/download), install the [remote containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers)
|
||||
1. In Visual Studio Code, press on `F1` and select `Remote-Containers: Open Folder in Container...`
|
||||
1. Your dev environment is ready to go!... and it's running in a container :+1:
|
||||
|
||||
### Locally
|
||||
|
||||
Install [Go](https://golang.org/dl/), [Docker](https://www.docker.com/products/docker-desktop) and [Git](https://git-scm.com/downloads); then:
|
||||
|
||||
```sh
|
||||
go mod download
|
||||
```
|
||||
|
||||
And finally install [golangci-lint](https://github.com/golangci/golangci-lint#install).
|
||||
|
||||
You might want to use an editor such as [Visual Studio Code](https://code.visualstudio.com/download) with the [Go extension](https://code.visualstudio.com/docs/languages/go).
|
||||
|
||||
## Commands available
|
||||
|
||||
- Test the code: `go test ./...`
|
||||
- Lint the code `golangci-lint run`
|
||||
- Build the program: `go build -o app cmd/updater/main.go`
|
||||
- Build the Docker image (tests and lint included): `docker build -t qmcgaw/ddns-updater .`
|
||||
- Run the Docker container: `docker run -it --rm -v /yourpath/data:/updater/data qmcgaw/ddns-updater`
|
||||
|
||||
## Add a new DNS provider
|
||||
|
||||
An "example" DNS provider is present in the code, you can simply copy paste it modify it to your needs.
|
||||
In more detailed steps:
|
||||
|
||||
1. Copy the directory [`internal/provider/providers/example`](../internal/provider/providers/example) to `internal/provider/providers/yourprovider` where `yourprovider` is the name of the DNS provider you want to add, in a single word without spaces, dashes or underscores.
|
||||
1. Modify the `internal/provider/providers/yourprovider/provider.go` file to fit the requirements of your DNS provider. There are many `// TODO` comments you can follow and **need to remove** when done.
|
||||
1. Add the provider name constant to the `ProviderChoices` function in [`internal/provider/constants/providers.go`](../internal/provider/constants/providers.go). For example:
|
||||
|
||||
```go
|
||||
func ProviderChoices() []models.Provider {
|
||||
return []models.Provider{
|
||||
// ...
|
||||
Example,
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
1. Add a case for your provider in the `switch` statement in the `New` function in [`internal/provider/provider.go`](../internal/provider/provider.go). For example:
|
||||
|
||||
```go
|
||||
case constants.Example:
|
||||
return example.New(data, domain, host, ipVersion, ipv6Suffix)
|
||||
```
|
||||
|
||||
1. Copy the file [`docs/example.md`](../docs/example.md) to `docs/yourprovider.md` and modify it to fit the configuration and domain setup of your DNS provider. There are a few `<!-- ... -->` comments indicating what to change, please **remove them** when done.
|
||||
1. In the [README.md](../README.md):
|
||||
1. Add your provider name to the list of providers supported `- Your provider`
|
||||
1. Add your provider name and link to its document to the second list: `- [Your provider](docs/yourprovider.md)`
|
||||
1. Make sure to run the actual program (in Docker or directly) and check it updates your DNS records as expected, of course 😉 You can do this by setting a record to `127.0.0.1` manually and then run the updater to see if the update succeeds.
|
||||
1. Profit 🎉 Don't forget to [open a pull request](https://github.com/qdm12/ddns-updater/compare)
|
||||
|
||||
## License
|
||||
|
||||
Contributions are [released](https://help.github.com/articles/github-terms-of-service/#6-contributions-under-repository-license) to the public under the [open source license of this project](../LICENSE).
|
||||
|
||||
@@ -376,7 +376,7 @@ You can use optional build arguments with `--build-arg KEY=VALUE` from the table
|
||||
|
||||
## Development and contributing
|
||||
|
||||
- [Contribute with code](docs/contributing.md)
|
||||
- [Contribute with code](.github/CONTRIBUTING.md)
|
||||
- [Github workflows to know what's building](https://github.com/qdm12/ddns-updater/actions)
|
||||
- [List of issues and feature requests](https://github.com/qdm12/ddns-updater/issues)
|
||||
- [Kanban board](https://github.com/qdm12/ddns-updater/projects/1)
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
# Contributing
|
||||
|
||||
## Table of content
|
||||
|
||||
1. [Setup](#setup)
|
||||
1. [Commands available](#commands-available)
|
||||
1. [Guidelines](#guidelines)
|
||||
|
||||
## Setup
|
||||
|
||||
### Using VSCode and Docker
|
||||
|
||||
That should be easier and better than a local setup, although it might use more memory if you're not on Linux.
|
||||
|
||||
1. Install [Docker](https://docs.docker.com/install/)
|
||||
- On Windows, share a drive with Docker Desktop and have the project on that partition
|
||||
- On OSX, share your project directory with Docker Desktop
|
||||
1. With [Visual Studio Code](https://code.visualstudio.com/download), install the [remote containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers)
|
||||
1. In Visual Studio Code, press on `F1` and select `Remote-Containers: Open Folder in Container...`
|
||||
1. Your dev environment is ready to go!... and it's running in a container :+1:
|
||||
|
||||
### Locally
|
||||
|
||||
Install [Go](https://golang.org/dl/), [Docker](https://www.docker.com/products/docker-desktop) and [Git](https://git-scm.com/downloads); then:
|
||||
|
||||
```sh
|
||||
go mod download
|
||||
```
|
||||
|
||||
And finally install [golangci-lint](https://github.com/golangci/golangci-lint#install).
|
||||
|
||||
You might want to use an editor such as [Visual Studio Code](https://code.visualstudio.com/download) with the [Go extension](https://code.visualstudio.com/docs/languages/go).
|
||||
|
||||
## Build and Run
|
||||
|
||||
```sh
|
||||
go build -o app cmd/updater/main.go
|
||||
./app
|
||||
```
|
||||
|
||||
## Commands available
|
||||
|
||||
- Test the code: `go test ./...`
|
||||
- Lint the code `golangci-lint run`
|
||||
- Build the Docker image (tests and lint included): `docker build -t qmcgaw/ddns-updater .`
|
||||
- Run the Docker container: `docker run -it --rm -v /yourpath/data:/updater/data qmcgaw/ddns-updater`
|
||||
|
||||
## Guidelines
|
||||
|
||||
The Go code is in the Go file [cmd/updater/main.go](../cmd/updater/main.go) and the [internal directory](../internal), you might want to start reading the main.go file.
|
||||
|
||||
See the [Contributing document](../.github/CONTRIBUTING.md) for more information on how to contribute to this repository.
|
||||
43
docs/example.md
Normal file
43
docs/example.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# Example.com
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
<!-- UPDATE THIS JSON EXAMPLE -->
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "example",
|
||||
"domain": "domain.com",
|
||||
"host": "@",
|
||||
"username": "username",
|
||||
"password": "password",
|
||||
"ip_version": "ipv4",
|
||||
"ipv6_suffix": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` is your host and can be a subdomain or `"@"` or the wildcard `"*"`
|
||||
- `"username"`
|
||||
- `"password"`
|
||||
|
||||
<!-- UPDATE THIS IF NEEDED -->
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"ip_version"` can be `ipv4` (A records), or `ipv6` (AAAA records) or `ipv4 or ipv6` (update one of the two, depending on the public ip found). It defaults to `ipv4 or ipv6`.
|
||||
- `"ipv6_suffix"` is the IPv6 interface identifiersuffix to use. It can be for example `0:0:0:0:72ad:8fbb:a54e:bedd/64`. If left empty, it defaults to no suffix and the raw public IPv6 address obtained is used in the record updating.
|
||||
|
||||
<!-- UPDATE THIS IF NEEDED -->
|
||||
|
||||
## Domain setup
|
||||
|
||||
<!-- FILL THIS UP WITH A FEW NUMBERED STEPS -->
|
||||
@@ -21,6 +21,7 @@ const (
|
||||
Dynu models.Provider = "dynu"
|
||||
DynV6 models.Provider = "dynv6"
|
||||
EasyDNS models.Provider = "easydns"
|
||||
Example models.Provider = "example"
|
||||
FreeDNS models.Provider = "freedns"
|
||||
Gandi models.Provider = "gandi"
|
||||
GCP models.Provider = "gcp"
|
||||
@@ -68,6 +69,7 @@ func ProviderChoices() []models.Provider {
|
||||
Dynu,
|
||||
DynV6,
|
||||
EasyDNS,
|
||||
Example,
|
||||
FreeDNS,
|
||||
Gandi,
|
||||
GCP,
|
||||
|
||||
@@ -11,10 +11,12 @@ var (
|
||||
ErrConsumerKeyNotSet = errors.New("consumer key is not set")
|
||||
ErrCredentialsNotSet = errors.New("credentials are not set")
|
||||
ErrCustomerNumberNotSet = errors.New("customer number is not set")
|
||||
ErrDomainNotSet = errors.New("domain is not set")
|
||||
ErrEmailNotSet = errors.New("email is not set")
|
||||
ErrEmailNotValid = errors.New("email address is not valid")
|
||||
ErrGCPProjectNotSet = errors.New("GCP project is not set")
|
||||
ErrDomainNotValid = errors.New("domain is not valid")
|
||||
ErrHostNotSet = errors.New("host is not set")
|
||||
ErrHostOnlySubdomain = errors.New("host can only be a subdomain")
|
||||
ErrHostWildcard = errors.New(`host cannot be a "*"`)
|
||||
ErrIPv4KeyNotSet = errors.New("IPv4 key is not set")
|
||||
|
||||
@@ -27,6 +27,7 @@ import (
|
||||
"github.com/qdm12/ddns-updater/internal/provider/providers/dynu"
|
||||
"github.com/qdm12/ddns-updater/internal/provider/providers/dynv6"
|
||||
"github.com/qdm12/ddns-updater/internal/provider/providers/easydns"
|
||||
"github.com/qdm12/ddns-updater/internal/provider/providers/example"
|
||||
"github.com/qdm12/ddns-updater/internal/provider/providers/freedns"
|
||||
"github.com/qdm12/ddns-updater/internal/provider/providers/gandi"
|
||||
"github.com/qdm12/ddns-updater/internal/provider/providers/gcp"
|
||||
@@ -109,6 +110,8 @@ func New(providerName models.Provider, data json.RawMessage, domain, host string
|
||||
return dynv6.New(data, domain, host, ipVersion, ipv6Suffix)
|
||||
case constants.EasyDNS:
|
||||
return easydns.New(data, domain, host, ipVersion, ipv6Suffix)
|
||||
case constants.Example:
|
||||
return example.New(data, domain, host, ipVersion, ipv6Suffix)
|
||||
case constants.FreeDNS:
|
||||
return freedns.New(data, domain, host, ipVersion, ipv6Suffix)
|
||||
case constants.Gandi:
|
||||
|
||||
182
internal/provider/providers/example/provider.go
Normal file
182
internal/provider/providers/example/provider.go
Normal file
@@ -0,0 +1,182 @@
|
||||
package example
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/ddns-updater/internal/provider/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/provider/errors"
|
||||
"github.com/qdm12/ddns-updater/internal/provider/headers"
|
||||
"github.com/qdm12/ddns-updater/internal/provider/utils"
|
||||
"github.com/qdm12/ddns-updater/pkg/publicip/ipversion"
|
||||
)
|
||||
|
||||
type Provider struct {
|
||||
domain string
|
||||
host string
|
||||
// TODO: remove ipVersion and ipv6Suffix if the provider does not support IPv6.
|
||||
// Usually they do support IPv6 though.
|
||||
ipVersion ipversion.IPVersion
|
||||
ipv6Suffix netip.Prefix
|
||||
username string
|
||||
password string
|
||||
}
|
||||
|
||||
func New(data json.RawMessage, domain, host string,
|
||||
ipVersion ipversion.IPVersion, ipv6Suffix netip.Prefix) (
|
||||
provider *Provider, err error) {
|
||||
var providerSpecificSettings settings
|
||||
err = json.Unmarshal(data, &providerSpecificSettings)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decoding provider specific settings: %w", err)
|
||||
}
|
||||
|
||||
err = validateSettings(providerSpecificSettings, domain, host)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("validating provider specific settings: %w", err)
|
||||
}
|
||||
|
||||
return &Provider{
|
||||
domain: domain,
|
||||
host: host,
|
||||
ipVersion: ipVersion,
|
||||
ipv6Suffix: ipv6Suffix,
|
||||
username: providerSpecificSettings.Username,
|
||||
password: providerSpecificSettings.Password,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// TODO adapt to the provider specific settings.
|
||||
type settings struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
func validateSettings(providerSpecificSettings settings, domain, host string) error {
|
||||
// TODO: update this switch to be as restrictive as possible
|
||||
// to fail early for the user. Use errors already defined
|
||||
// in the internal/provider/errors package, or add your own
|
||||
// if really necessary. When returning an error, always use
|
||||
// fmt.Errorf (to enforce the caller to use errors.Is()).
|
||||
switch {
|
||||
case domain == "":
|
||||
return fmt.Errorf("%w", errors.ErrDomainNotSet)
|
||||
case host == "":
|
||||
return fmt.Errorf("%w", errors.ErrHostNotSet)
|
||||
// TODO: does the provider support wildcard hosts? If not, disallow * hosts
|
||||
// case host == "*":
|
||||
// return fmt.Errorf("%w", errors.ErrHostWildcard)
|
||||
case providerSpecificSettings.Username == "":
|
||||
return fmt.Errorf("%w", errors.ErrUsernameNotSet)
|
||||
case providerSpecificSettings.Password == "":
|
||||
return fmt.Errorf("%w", errors.ErrPasswordNotSet)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Provider) String() string {
|
||||
// TODO update the name of the provider and add it to the
|
||||
// internal/provider/constants package.
|
||||
return utils.ToString(p.domain, p.host, constants.Dyn, p.ipVersion)
|
||||
}
|
||||
|
||||
func (p *Provider) Domain() string {
|
||||
return p.domain
|
||||
}
|
||||
|
||||
func (p *Provider) Host() string {
|
||||
return p.host
|
||||
}
|
||||
|
||||
func (p *Provider) IPVersion() ipversion.IPVersion {
|
||||
return p.ipVersion
|
||||
}
|
||||
|
||||
func (p *Provider) IPv6Suffix() netip.Prefix {
|
||||
return p.ipv6Suffix
|
||||
}
|
||||
|
||||
func (p *Provider) Proxied() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *Provider) BuildDomainName() string {
|
||||
return utils.BuildDomainName(p.host, p.domain)
|
||||
}
|
||||
|
||||
func (p *Provider) HTML() models.HTMLRow {
|
||||
return models.HTMLRow{
|
||||
Domain: fmt.Sprintf("<a href=\"http://%s\">%s</a>", p.BuildDomainName(), p.BuildDomainName()),
|
||||
Host: p.Host(),
|
||||
// TODO: update the provider name and link below
|
||||
Provider: "<a href=\"https://dyn.com/\">Dyn DNS</a>",
|
||||
IPVersion: p.ipVersion.String(),
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: update this function to match the provider's API
|
||||
// Ideally add a comment with a link to their API documentation.
|
||||
// If the provider API allows it, create the record if it does not exist.
|
||||
func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Addr) (newIP netip.Addr, err error) {
|
||||
u := url.URL{
|
||||
Scheme: "https",
|
||||
User: url.UserPassword(p.username, p.password),
|
||||
Host: "example.com",
|
||||
Path: "/nic/update",
|
||||
}
|
||||
values := url.Values{}
|
||||
values.Set("hostname", utils.BuildURLQueryHostname(p.host, p.domain))
|
||||
values.Set("myip", ip.String())
|
||||
u.RawQuery = values.Encode()
|
||||
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
|
||||
if err != nil {
|
||||
return netip.Addr{}, fmt.Errorf("creating http request: %w", err)
|
||||
}
|
||||
// TODO: there are other helping functions in the headers package to set request headers
|
||||
// if you need them.
|
||||
headers.SetUserAgent(request)
|
||||
|
||||
response, err := client.Do(request)
|
||||
if err != nil {
|
||||
return netip.Addr{}, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
// TODO handle the encoding of the response body properly. Often it can be JSON,
|
||||
// see other provider code for examples on how to decode JSON.
|
||||
b, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return netip.Addr{}, fmt.Errorf("reading response body: %w", err)
|
||||
}
|
||||
s := string(b)
|
||||
|
||||
// TODO handle every possible status codes from the provider API.
|
||||
// If undocumented, try them out by sending bogus HTTP requests to see
|
||||
// what status codes they return, for example with `curl`.
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return netip.Addr{}, fmt.Errorf("%w: %d: %s",
|
||||
errors.ErrHTTPStatusNotValid, response.StatusCode, utils.ToSingleLine(s))
|
||||
}
|
||||
|
||||
// TODO handle every possible response bodies from the provider API.
|
||||
// If undocumented, try them out by sending bogus HTTP requests to see
|
||||
// what response bodies they return, for example with `curl`.
|
||||
switch {
|
||||
case strings.HasPrefix(s, constants.Notfqdn):
|
||||
return netip.Addr{}, fmt.Errorf("%w", errors.ErrHostnameNotExists)
|
||||
case strings.HasPrefix(s, "badrequest"):
|
||||
return netip.Addr{}, fmt.Errorf("%w", errors.ErrBadRequest)
|
||||
case strings.HasPrefix(s, "good"):
|
||||
return ip, nil
|
||||
default:
|
||||
return netip.Addr{}, fmt.Errorf("%w: %s", errors.ErrUnknownResponse, s)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user