From 49972f5502b7269964de6f9aef2ca978ba6ba9e3 Mon Sep 17 00:00:00 2001 From: Jared Lunde Date: Sun, 8 Sep 2024 23:10:42 -0700 Subject: [PATCH] feat: draw the owl --- .github/ISSUE_TEMPLATE/bug_report.md | 38 +++++ .github/ISSUE_TEMPLATE/feature_request.md | 20 +++ .github/workflows/pr.yml | 29 ++++ .github/workflows/release.yml | 31 ++++ .gitignore | 4 + .goreleaser.yaml | 36 +++++ CODE_OF_CONDUCT.md | 128 ++++++++++++++++ README.md | 39 ++++- cmd/envtpl/main.go | 145 ++++++++++++++++++ go.mod | 33 ++++ go.sum | 134 +++++++++++++++++ main.go | 175 ++++++++++++++++++++++ pkg/nanoid/nanoid.go | 70 +++++++++ pkg/nanoid/nanoid_test.go | 33 ++++ testdata/.env.template | 13 ++ 15 files changed, 926 insertions(+), 2 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/workflows/pr.yml create mode 100644 .github/workflows/release.yml create mode 100644 .goreleaser.yaml create mode 100644 CODE_OF_CONDUCT.md create mode 100644 cmd/envtpl/main.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 pkg/nanoid/nanoid.go create mode 100644 pkg/nanoid/nanoid_test.go create mode 100644 testdata/.env.template diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..dd84ea7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,38 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. iOS] + - Browser [e.g. chrome, safari] + - Version [e.g. 22] + +**Smartphone (please complete the following information):** + - Device: [e.g. iPhone6] + - OS: [e.g. iOS8.1] + - Browser [e.g. stock browser, safari] + - Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..bbcbbe7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml new file mode 100644 index 0000000..cf95b56 --- /dev/null +++ b/.github/workflows/pr.yml @@ -0,0 +1,29 @@ +name: Pull request +on: + pull_request: + branches: + - main + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + check: + name: Check + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + - name: Setup Go ${{ matrix.go-version }} + uses: actions/setup-go@v5 + with: + go-version: 1.23.x + - name: Install dependencies + run: go mod download + - name: Lint + run: go vet ./... + - name: Run tests + run: go test -v ./... diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..0bb1543 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,31 @@ +name: Release + +on: + push: + tags: + - "*" + branches: + - main + +permissions: + contents: write + +jobs: + release: + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/v') + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Set up Go + uses: actions/setup-go@v4 + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@v6 + with: + distribution: goreleaser + version: "~> v2" + args: release --clean + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 6f72f89..127790e 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,7 @@ go.work.sum # env file .env +/.env.* + +# goreleaser dist +dist/ \ No newline at end of file diff --git a/.goreleaser.yaml b/.goreleaser.yaml new file mode 100644 index 0000000..b6ed10e --- /dev/null +++ b/.goreleaser.yaml @@ -0,0 +1,36 @@ +version: 2 + +before: + hooks: + - go mod tidy + +builds: + - main: ./cmd/envtpl + env: + - CGO_ENABLED=0 + goos: + - linux + - windows + - darwin + +archives: + - format: tar.gz + # this name template makes the OS and Arch compatible with the results of `uname`. + name_template: >- + {{ .ProjectName }}- + {{- .Os }}- + {{- if eq .Arch "amd64" }}x86_64 + {{- else if eq .Arch "386" }}i386 + {{- else }}{{ .Arch }}{{ end }} + {{- if .Arm }}v{{ .Arm }}{{ end }} + # use zip for windows archives + format_overrides: + - goos: windows + format: zip + +changelog: + sort: asc + filters: + exclude: + - "^docs:" + - "^test:" diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..589aa57 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +jared@flexstack.com. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/README.md b/README.md index 0be91ba..2f52abd 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,37 @@ -# env-template -A CLI tool for generating .env files from .env.template files via prompt +# envtpl + +A CLI tool for generating .env files from .env.template files via prompts and random values. + +## Usage + +```sh +envtpl [template-file] -o [output-file] +``` + +### Supported types + +```sh +# .env.template +UUID= +PASSWORD= +ENCRYPTION_KEY= +ENCRYPTION_IV= +ENCRYPTION_SALT= +PORT= +ENUM= +PROMPT= +SECRET_PROMPT= +PROMPT_2= # Empty values elicit a prompt +``` + +| Type | Description | Example | +| --- | --- | --- | +| alpha | Random alphanumeric characters | `PASSWORD=` | +| ascii85 | Random ASCII characters | `ENCRYPTION_KEY=` | +| base64 | Random base64 characters | `ENCRYPTION_IV=` | +| hex | Random hexadecimal characters | `ENCRYPTION_SALT=` | +| uuid | Random UUIDv4 | `UUID=` | +| enum | Enumerated list of values | `ENUM=` | +| int | Random integer | `PORT=` | +| text | Prompt user for text input | `PROMPT1=` | +| password | Prompt user for secret input | `SECRET_PROMPT=` | diff --git a/cmd/envtpl/main.go b/cmd/envtpl/main.go new file mode 100644 index 0000000..1224a2c --- /dev/null +++ b/cmd/envtpl/main.go @@ -0,0 +1,145 @@ +package main + +import ( + "errors" + + "fmt" + "os" + "path/filepath" + + "github.com/flexstack/envtpl" + "github.com/joho/godotenv" + "github.com/pterm/pterm" + flag "github.com/spf13/pflag" +) + +func main() { + // The first argument is the path to the template file + var file string + flag.StringVarP(&file, "output", "o", ".env", "The path to output the .env file") + flag.Parse() + + path := flag.Arg(0) + if path == "" { + defaultPaths := []string{".env.template", ".env.example"} + for _, p := range defaultPaths { + if _, err := os.Stat(p); err == nil { + path = p + break + } + } + } + + // Check if the path is a directory + if fi, err := os.Stat(path); err != nil || fi.IsDir() { + defaultPaths := []string{".env.template", ".env.example"} + found := false + + for _, p := range defaultPaths { + maybePath := filepath.Join(path, p) + + if _, err := os.Stat(maybePath); err == nil { + path = maybePath + found = true + break + } + } + + if !found { + fmt.Println("No template file found") + os.Exit(1) + } + } + + if path == "" { + fmt.Println("No template file found") + os.Exit(1) + } + + // Parse the template file + envVars, err := envtpl.Parse(path) + if err != nil { + fmt.Println("Error parsing template file:", path) + os.Exit(1) + } + + env := make(map[string]string) + + // If the .env file already exists, merge the contents + if _, err = os.Stat(file); err == nil { + existing, err := godotenv.Read(file) + if err != nil { + fmt.Println("Error reading existing .env file:", err) + os.Exit(1) + } + + for key, value := range existing { + env[key] = value + } + } + + hasChanges := false + for _, envVar := range envVars { + if env[envVar.Key] != "" { + continue + } + + hasChanges = true + value, err := envVar.Value.Generate() + if err != nil { + fmt.Println("Error generating value for:", envVar.Key) + os.Exit(1) + } + switch envVar.Value.Type { + case envtpl.Text, envtpl.Password: + prompt := envVar.Key + v, _ := envVar.Value.Generate() + if p, ok := v.(string); ok && p != "" { + prompt = p + } + var result string + if envVar.Value.Type == envtpl.Password { + result, _ = pterm.DefaultInteractiveTextInput.WithMask("*").Show(prompt) + } else { + result, _ = pterm.DefaultInteractiveTextInput.Show(prompt) + } + env[envVar.Key] = result + + case envtpl.Enum: + rawEnum, err := envVar.Value.Generate() + if errors.Is(err, envtpl.ErrInvalidArg) { + fmt.Println("Error generating value for:", envVar.Key) + os.Exit(1) + } + enum, ok := rawEnum.([]string) + if !ok { + fmt.Println("Error generating value for:", envVar.Key) + os.Exit(1) + } + result, _ := pterm.DefaultInteractiveSelect.WithOptions(enum).WithFilter(false).Show(envVar.Key) + env[envVar.Key] = result + + default: + env[envVar.Key] = value.(string) + } + } + + if !hasChanges { + fmt.Println("No changes to .env file") + os.Exit(0) + } + + // Generate the .env + contents, err := godotenv.Marshal(env) + if err != nil { + fmt.Println("Error generating .env file:", err) + os.Exit(1) + } + + // Write the .env file + fmt.Println("Writing to:", file) + if err = os.WriteFile(file, []byte(contents), 0644); err != nil { + fmt.Println("Error writing .env file:", err) + os.Exit(1) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..fd08168 --- /dev/null +++ b/go.mod @@ -0,0 +1,33 @@ +module github.com/flexstack/envtpl + +go 1.23.1 + +require ( + github.com/c-bata/go-prompt v0.2.6 + github.com/flexstack/uuid v1.0.0 + github.com/joho/godotenv v1.5.1 +) + +require ( + atomicgo.dev/cursor v0.2.0 // indirect + atomicgo.dev/keyboard v0.2.9 // indirect + atomicgo.dev/schedule v0.1.0 // indirect + github.com/containerd/console v1.0.3 // indirect + github.com/gookit/color v1.5.4 // indirect + github.com/lithammer/fuzzysearch v1.1.8 // indirect + github.com/rivo/uniseg v0.4.4 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + golang.org/x/term v0.16.0 // indirect + golang.org/x/text v0.14.0 // indirect +) + +require ( + github.com/mattn/go-colorable v0.1.7 // indirect + github.com/mattn/go-isatty v0.0.12 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/mattn/go-tty v0.0.3 // indirect + github.com/pkg/term v1.2.0-beta.2 // indirect + github.com/pterm/pterm v0.12.79 + github.com/spf13/pflag v1.0.5 + golang.org/x/sys v0.16.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..1d6f364 --- /dev/null +++ b/go.sum @@ -0,0 +1,134 @@ +atomicgo.dev/cursor v0.2.0 h1:H6XN5alUJ52FZZUkI7AlJbUc1aW38GWZalpYRPpoPOw= +atomicgo.dev/cursor v0.2.0/go.mod h1:Lr4ZJB3U7DfPPOkbH7/6TOtJ4vFGHlgj1nc+n900IpU= +atomicgo.dev/keyboard v0.2.9 h1:tOsIid3nlPLZ3lwgG8KZMp/SFmr7P0ssEN5JUsm78K8= +atomicgo.dev/keyboard v0.2.9/go.mod h1:BC4w9g00XkxH/f1HXhW2sXmJFOCWbKn9xrOunSFtExQ= +atomicgo.dev/schedule v0.1.0 h1:nTthAbhZS5YZmgYbb2+DH8uQIZcTlIrd4eYr3UQxEjs= +atomicgo.dev/schedule v0.1.0/go.mod h1:xeUa3oAkiuHYh8bKiQBRojqAMq3PXXbJujjb0hw8pEU= +github.com/MarvinJWendt/testza v0.1.0/go.mod h1:7AxNvlfeHP7Z/hDQ5JtE3OKYT3XFUeLCDE2DQninSqs= +github.com/MarvinJWendt/testza v0.2.1/go.mod h1:God7bhG8n6uQxwdScay+gjm9/LnO4D3kkcZX4hv9Rp8= +github.com/MarvinJWendt/testza v0.2.8/go.mod h1:nwIcjmr0Zz+Rcwfh3/4UhBp7ePKVhuBExvZqnKYWlII= +github.com/MarvinJWendt/testza v0.2.10/go.mod h1:pd+VWsoGUiFtq+hRKSU1Bktnn+DMCSrDrXDpX2bG66k= +github.com/MarvinJWendt/testza v0.2.12/go.mod h1:JOIegYyV7rX+7VZ9r77L/eH6CfJHHzXjB69adAhzZkI= +github.com/MarvinJWendt/testza v0.3.0/go.mod h1:eFcL4I0idjtIx8P9C6KkAuLgATNKpX4/2oUqKc6bF2c= +github.com/MarvinJWendt/testza v0.4.2/go.mod h1:mSdhXiKH8sg/gQehJ63bINcCKp7RtYewEjXsvsVUPbE= +github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk= +github.com/c-bata/go-prompt v0.2.6 h1:POP+nrHE+DfLYx370bedwNhsqmpCUynWPxuHi0C5vZI= +github.com/c-bata/go-prompt v0.2.6/go.mod h1:/LMAke8wD2FsNu9EXNdHxNLbd9MedkPnCdfpU9wwHfY= +github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw= +github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/flexstack/uuid v1.0.0 h1:iJCndNC/1NyRW5vEFu3aR7HmZLBT9fxEbEFgaFI6nZ4= +github.com/flexstack/uuid v1.0.0/go.mod h1:Ssmzr+osP1+Ee+s6eXC9YIbUK3aZVtp8gT5bzmKf6K8= +github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ= +github.com/gookit/color v1.5.0/go.mod h1:43aQb+Zerm/BWh2GnrgOQm7ffz7tvQXEKV6BFMl7wAo= +github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0= +github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= +github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4= +github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.7 h1:bQGKb3vps/j0E9GfJQ03JyhRuxsvdAanXlT9BTw3mdw= +github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= +github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-runewidth v0.0.6/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-tty v0.0.3 h1:5OfyWorkyO7xP52Mq7tB36ajHDG5OHrmBGIS/DtakQI= +github.com/mattn/go-tty v0.0.3/go.mod h1:ihxohKRERHTVzN+aSVRwACLCeqIoZAWpoICkkvrWyR0= +github.com/pkg/term v1.2.0-beta.2 h1:L3y/h2jkuBVFdWiJvNfYfKmzcCnILw7mJWm2JQuMppw= +github.com/pkg/term v1.2.0-beta.2/go.mod h1:E25nymQcrSllhX42Ok8MRm1+hyBdHY0dCeiKZ9jpNGw= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pterm/pterm v0.12.27/go.mod h1:PhQ89w4i95rhgE+xedAoqous6K9X+r6aSOI2eFF7DZI= +github.com/pterm/pterm v0.12.29/go.mod h1:WI3qxgvoQFFGKGjGnJR849gU0TsEOvKn5Q8LlY1U7lg= +github.com/pterm/pterm v0.12.30/go.mod h1:MOqLIyMOgmTDz9yorcYbcw+HsgoZo3BQfg2wtl3HEFE= +github.com/pterm/pterm v0.12.31/go.mod h1:32ZAWZVXD7ZfG0s8qqHXePte42kdz8ECtRyEejaWgXU= +github.com/pterm/pterm v0.12.33/go.mod h1:x+h2uL+n7CP/rel9+bImHD5lF3nM9vJj80k9ybiiTTE= +github.com/pterm/pterm v0.12.36/go.mod h1:NjiL09hFhT/vWjQHSj1athJpx6H8cjpHXNAK5bUw8T8= +github.com/pterm/pterm v0.12.40/go.mod h1:ffwPLwlbXxP+rxT0GsgDTzS3y3rmpAO1NMjUkGTYf8s= +github.com/pterm/pterm v0.12.79 h1:lH3yrYMhdpeqX9y5Ep1u7DejyHy7NSQg9qrBjF9dFT4= +github.com/pterm/pterm v0.12.79/go.mod h1:1v/gzOF1N0FsjbgTHZ1wVycRkKiatFvJSJC4IGaQAAo= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= +github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200918174421-af09f7315aff h1:1CPUrky56AcgSpxz/KfgzQWzfG09u5YOL8MvPYBlrL8= +golang.org/x/sys v0.0.0-20200918174421-af09f7315aff/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE= +golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..6d91914 --- /dev/null +++ b/main.go @@ -0,0 +1,175 @@ +package envtpl + +import ( + "cmp" + "errors" + "fmt" + "math/rand" + "regexp" + "sort" + "strconv" + "strings" + + "github.com/flexstack/envtpl/pkg/nanoid" + "github.com/flexstack/uuid" + "github.com/joho/godotenv" +) + +func Parse(file string) ([]EnvVar, error) { + env, err := godotenv.Read(file) + if err != nil { + return nil, err + } + + envVars := make([]EnvVar, len(env)) + i := 0 + for key, value := range env { + val := ParseValue(value) + envVars[i] = EnvVar{Key: key, Value: val} + i++ + } + + // Sort the environment variables by key + sort.Slice(envVars, func(i, j int) bool { + return cmp.Less(envVars[i].Key, envVars[j].Key) + }) + + return envVars, nil +} + +func ParseValue(value string) *EnvValue { + if value == "" { + return &EnvValue{ + Type: Text, + value: "", + } + } + + matches := valueRegex.FindStringSubmatch(value) + if matches == nil { + return &EnvValue{ + Type: Plain, + value: value, + } + } + + groups := make(map[string]string) + for i, name := range valueRegex.SubexpNames() { + if i != 0 && name != "" { + groups[name] = matches[i] + } + } + + return &EnvValue{ + Type: ValueType(groups["type"]), + value: groups["value"], + } +} + +var valueRegex = regexp.MustCompile(`<(?Ptext|password|enum|uuid|alpha|hex|base64|ascii85|int)(:(?P.*))?>`) + +type EnvVar struct { + Key string + Value *EnvValue +} + +type EnvValue struct { + Type ValueType + value string +} + +func (v *EnvValue) Generate() (interface{}, error) { + if v.Type == Plain { + return v.value, nil + } + + switch v.Type { + case Text, Password: + return v.value, nil + + case Enum: + possibleValues := strings.Split(v.value, ",") + if len(possibleValues) == 0 { + return nil, ErrInvalidArg + } + for i, value := range possibleValues { + possibleValues[i] = strings.TrimSpace(value) + } + return possibleValues, nil + + case UUID: + return uuid.Must(uuid.NewV4()).String(), nil + + case Int: + min := int64(0) + max := int64(100) + var err error + if v.value != "" { + if strings.Contains(v.value, "-") { + intRange := strings.Split(v.value, "-") + if len(intRange) != 2 { + return nil, ErrInvalidArg + } + min, err = strconv.ParseInt(intRange[0], 10, 64) + if err != nil { + return nil, ErrInvalidArg + } + max, err = strconv.ParseInt(intRange[1], 10, 64) + if err != nil { + return nil, ErrInvalidArg + } + } + } else { + max, err = strconv.ParseInt(v.value, 10, 64) + if err != nil { + return nil, ErrInvalidArg + } + } + + return fmt.Sprint(rand.Intn(int(max-min)) + int(min)), nil + + case Alpha, Hex, Base64, Ascii85: + length := 16 + + if v.value != "" { + l, err := strconv.ParseInt(v.value, 10, 64) + if err != nil { + return nil, ErrInvalidArg + } + length = int(l) + } + + alphabet := nanoid.AlphabetDefault + switch v.Type { + case Hex: + alphabet = nanoid.AlphabetHex + case Base64: + alphabet = nanoid.AlphabetBase64 + case Ascii85: + alphabet = nanoid.AlphabetAscii85 + } + + return nanoid.New(length, alphabet), nil + } + + return nil, ErrInvalidArg +} + +type ValueType string + +const ( + Plain ValueType = "plain" + Text ValueType = "text" + Password ValueType = "password" + Enum ValueType = "enum" + UUID ValueType = "uuid" + Alpha ValueType = "alpha" + Hex ValueType = "hex" + Base64 ValueType = "base64" + Ascii85 ValueType = "ascii85" + Int ValueType = "int" +) + +var ( + ErrInvalidArg = errors.New("invalid argument") +) diff --git a/pkg/nanoid/nanoid.go b/pkg/nanoid/nanoid.go new file mode 100644 index 0000000..d9cc43d --- /dev/null +++ b/pkg/nanoid/nanoid.go @@ -0,0 +1,70 @@ +// Credit: https://github.com/matoous/go-nanoid +// Credit: https://github.com/ai/nanoid +package nanoid + +import ( + "crypto/rand" + "math" +) + +// defaultAlphabet is the alphabet used for ID characters by default. +var ( + AlphabetDefault = []rune("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") + AlphabetBase64 = []rune("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/") + AlphabetHex = []rune("0123456789abcdef") + AlphabetAscii85 = []rune("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!#$%&()*+-;<=>?@^_`{|}~") +) + +// getMask generates bit mask used to obtain bits from the random bytes that are used to get index of random character +// from the alphabet. Example: if the alphabet has 6 = (110)_2 characters it is sufficient to use mask 7 = (111)_2 +func getMask(alphabetSize int) int { + for i := 1; i <= 8; i++ { + mask := (2 << uint(i)) - 1 + if mask >= alphabetSize-1 { + return mask + } + } + return 0 +} + +// Generate is a low-level function to change alphabet and ID size. +func Generate(alphabet string, size int) string { + chars := []rune(alphabet) + mask := getMask(len(chars)) + // estimate how many random bytes we will need for the ID, we might actually need more but this is tradeoff + // between average case and worst case + ceilArg := 1.6 * float64(mask*size) / float64(len(alphabet)) + step := int(math.Ceil(ceilArg)) + id := make([]rune, size) + bytes := make([]byte, step) + + for j := 0; ; { + _, err := rand.Read(bytes) + + if err != nil { + panic(err) + } + + for i := 0; i < step; i++ { + currByte := bytes[i] & byte(mask) + if currByte < byte(len(chars)) { + id[j] = chars[currByte] + j++ + if j == size { + return string(id[:size]) + } + } + } + } +} + +func New(size int, alphabet ...[]rune) string { + var chars []rune + if len(alphabet) == 0 { + chars = AlphabetDefault + } else { + chars = alphabet[0] + } + + return Generate(string(chars), size) +} diff --git a/pkg/nanoid/nanoid_test.go b/pkg/nanoid/nanoid_test.go new file mode 100644 index 0000000..f83c15b --- /dev/null +++ b/pkg/nanoid/nanoid_test.go @@ -0,0 +1,33 @@ +package nanoid + +import ( + "testing" +) + +func TestNew(t *testing.T) { + t.Run("default length", func(t *testing.T) { + id := New(21) + + if len(id) != 21 { + t.Errorf("expected %d, got %d", 21, len(id)) + } + }) + + t.Run("custom length", func(t *testing.T) { + for i := 1; i < 1024; i++ { + id := New(i) + + if len(id) != i { + t.Errorf("expected %d, got %d", i, len(id)) + } + } + }) + + t.Run("custom alphabet", func(t *testing.T) { + id := New(24, AlphabetAscii85) + + if len(id) != 24 { + t.Errorf("expected %d, got %d", 24, len(id)) + } + }) +} diff --git a/testdata/.env.template b/testdata/.env.template new file mode 100644 index 0000000..4a42d60 --- /dev/null +++ b/testdata/.env.template @@ -0,0 +1,13 @@ +UUID= +ENCRYPTION_KEY= +ENCRYPTION_IV= +ENCRYPTION_SALT= +PASSWORD= +PORT= +ENUM= +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= +PROMPT3= +IGNORE_ME="We are good dawg" +IGNORE_ME_TOO=6379 +AND_ME= \ No newline at end of file