Initial commit
All checks were successful
Codespell / Check for spelling errors (push) Successful in 23s

Signed-off-by: Sagi Dayan <sagidayan@gmail.com>
This commit is contained in:
Sagi Dayan 2024-11-22 17:29:25 +02:00
commit 693350fa49
Signed by: sagi
GPG key ID: FAB96BFC63B46458
59 changed files with 2277 additions and 0 deletions

View file

@ -0,0 +1,27 @@
name: Codespell
on:
push:
branches: [main]
pull_request:
branches: [main]
permissions:
contents: read
jobs:
codespell:
name: Check for spelling errors
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Annotate locations with typos
uses: "https://github.com/codespell-project/codespell-problem-matcher@v1"
- name: Codespell
uses: "https://github.com/codespell-project/actions-codespell@v2"
with:
check_filenames: true
check_hidden: true
skip: ./vendor

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
build/
vendor/
# test secrets
.secrets

46
Makefile Normal file
View file

@ -0,0 +1,46 @@
BINARY_NAME=envoid
VERSION=1.0.0-alpha
BUILD_FOLDER=build
GO_BUILD_LD_FLAGS=-ldflags="-s -w -X 'git.dayanhub.com/sagi/envoid/internal/variables.Commit=$(shell git rev-parse --short HEAD)' \
-X 'git.dayanhub.com/sagi/envoid/internal/variables.Version=${VERSION}'"
.PHONY: build
build:
GOARCH=amd64 GOOS=linux go build ${GO_BUILD_LD_FLAGS} -o ${BUILD_FOLDER}/${BINARY_NAME}-linux main.go
GOARCH=amd64 GOOS=darwin go build ${GO_BUILD_LD_FLAGS} -o ${BUILD_FOLDER}/${BINARY_NAME}-darwin main.go
GOARCH=amd64 GOOS=windows go build ${GO_BUILD_LD_FLAGS} -o ${BUILD_FOLDER}/${BINARY_NAME}-windows main.go
.PHONY: clean
clean:
go clean
rm -rf ${BUILD_FOLDER}
rm -rf ./docs
.PHONY: test
test:
go test ./...
.PHONY: test_coverage
test_coverage:
go test ./... -coverprofile=coverage.out
.PHONY: dep
dep:
go mod tidy
go mod vendor
.PHONY: vet
vet:
go vet
.PHONY: lint
lint:
golangci-lint run --enable-all
.PHONY: doc-gen
doc-gen:
rm -rf ./docs
mkdir ./docs
go run . doc

7
README.md Normal file
View file

@ -0,0 +1,7 @@
# Envoid
##### The `.env`(s) manager you never knew you needed
⚠ WIP !
[Documentation](docs/envoid.md)

145
cmd/env.go Normal file
View file

@ -0,0 +1,145 @@
package cmd
import (
"fmt"
"strings"
"git.dayanhub.com/sagi/envoid/internal/datastore"
"git.dayanhub.com/sagi/envoid/internal/errors"
"github.com/spf13/cobra"
)
type envCmdFlags struct {
rmEnvName *string
addEnvName *string
baseEnvName *string
}
var envFlags = &envCmdFlags{}
var envCmd = &cobra.Command{
Use: "env",
Short: "manage environments",
Long: "",
}
var rmEnvCmd = &cobra.Command{
Use: "rm",
Short: "removes an environment",
Long: "",
RunE: func(cmd *cobra.Command, args []string) error {
if err := initProject(); err != nil {
return err
}
ds, err := datastore.NewDataStore()
if err != nil {
return err
}
defer ds.Close()
err = ds.RemoveEnv(*envFlags.rmEnvName)
if err != nil {
return err
}
project.RemoveEnv(*envFlags.rmEnvName)
configuration.Save()
return nil
},
}
var addEnvCmd = &cobra.Command{
Use: "add",
Short: "adds an environment",
Long: "",
RunE: func(cmd *cobra.Command, args []string) error {
if err := initProject(); err != nil {
return err
}
if len(strings.TrimSpace(*envFlags.addEnvName)) == 0 {
return errors.NewInvalidFlagValueError("environment", *envFlags.addEnvName)
}
if _, err := project.GetEnv(*envFlags.addEnvName); err == nil {
return errors.NewEnvironmentExistsError(*envFlags.addEnvName)
}
ds, err := datastore.NewDataStore()
if err != nil {
return err
}
defer ds.Close()
if len(*envFlags.baseEnvName) != 0 {
_, err = project.GetEnv(*envFlags.baseEnvName)
if err != nil {
return err
}
err = ds.CreateEnvOffExsisting(*envFlags.addEnvName, *envFlags.baseEnvName)
if err != nil {
return err
}
} else {
err = ds.CreateEnv(*envFlags.addEnvName)
if err != nil {
return err
}
}
err = project.NewEnv(*envFlags.addEnvName)
if err != nil {
return err
}
configuration.Save()
return nil
},
}
var lsEnvCmd = &cobra.Command{
Use: "ls",
Short: "list all environments in this project",
Long: "",
RunE: func(cmd *cobra.Command, args []string) error {
if err := initProject(); err != nil {
return err
}
if project.IsEmpty() {
return errors.NewProjectEmptyError(project.Name)
}
for _, env := range project.Environments {
fmt.Printf("%s\n", env.Name)
}
return nil
},
}
func init() {
// add
envFlags.addEnvName = addEnvCmd.Flags().StringP("environment", "e", "", "environment name")
err := addEnvCmd.MarkFlagRequired("environment")
if err != nil {
panic(err)
}
envFlags.baseEnvName = addEnvCmd.Flags().StringP("base-environment", "b", "", "base environment name (copy data from base)")
err = addEnvCmd.RegisterFlagCompletionFunc("base-environment", validEnvironmentNamesComplete)
if err != nil {
panic(err)
}
//rm
envFlags.rmEnvName = rmEnvCmd.Flags().StringP("environment", "e", "", "environment name")
err = rmEnvCmd.MarkFlagRequired("environment")
if err != nil {
panic(err)
}
err = rmEnvCmd.RegisterFlagCompletionFunc("environment", validEnvironmentNamesComplete)
if err != nil {
panic(err)
}
// ls
envCmd.AddCommand(addEnvCmd)
envCmd.AddCommand(rmEnvCmd)
envCmd.AddCommand(lsEnvCmd)
}

81
cmd/get.go Normal file
View file

@ -0,0 +1,81 @@
package cmd
import (
"fmt"
"os"
"errors"
"git.dayanhub.com/sagi/envoid/internal/datastore"
intErrors "git.dayanhub.com/sagi/envoid/internal/errors"
"github.com/spf13/cobra"
)
type getCmdFlags struct {
envName *string
}
var getFlags = getCmdFlags{}
var getCmd = &cobra.Command{
Use: "get <key>",
Short: "Gets a variable",
Long: "",
RunE: func(cmd *cobra.Command, args []string) error {
err := initProject()
if err != nil {
return err
}
if project.IsEmpty() {
return intErrors.NewProjectEmptyError(project.Name)
}
if len(args) != 1 {
return intErrors.NewInvalidCommandError("expected 1 args. <key>")
}
key := args[0]
env := project.Environments[0]
err = checkAmbiguousEnv(*getFlags.envName)
if err != nil {
return err
}
if len(*getFlags.envName) > 0 {
e, err := project.GetEnv(*getFlags.envName)
if err != nil {
return err
}
env = e
}
ds, err := datastore.NewDataStore()
if err != nil {
fmt.Printf("Error: %e", err)
}
defer ds.Close()
envVar, err := ds.GetVar(env.Name, key)
if err != nil {
fmt.Printf("Error: %s\n", err.Error())
if errors.Is(err, &intErrors.NoKeyFoundError{}) {
os.Exit(1)
} else if errors.Is(err, intErrors.NewInvalidPasswordError()) {
os.Exit(1)
} else {
os.Exit(1)
}
}
fmt.Println(envVar.Value)
return nil
},
}
func init() {
getFlags.envName = getCmd.Flags().StringP("environment", "e", "", "environment name")
err := getCmd.RegisterFlagCompletionFunc("environment", validEnvironmentNamesComplete)
if err != nil {
panic(err)
}
}

77
cmd/import.go Normal file
View file

@ -0,0 +1,77 @@
package cmd
import (
"fmt"
"os"
"git.dayanhub.com/sagi/envoid/internal/datastore"
"git.dayanhub.com/sagi/envoid/internal/errors"
"git.dayanhub.com/sagi/envoid/internal/types"
"github.com/joho/godotenv"
"github.com/spf13/cobra"
)
type importCmdFlags struct {
envName *string
}
var importFlags = importCmdFlags{}
var importCmd = &cobra.Command{
Use: "import <file>",
Short: "import a .env file into environment(s)",
Long: "This will not encrypt any value. You can then use `set encrypt KEY_NAME` to encrypt",
RunE: func(cmd *cobra.Command, args []string) error {
err := initProject()
if err != nil {
return err
}
if project.IsEmpty() {
return errors.NewProjectEmptyError(project.Name)
}
envs := project.Environments
if len(*importFlags.envName) != 0 {
e, err := project.GetEnv(*importFlags.envName)
if err != nil {
return err
}
envs = []*types.Environment{e}
}
if len(args) != 1 {
return fmt.Errorf("Needs a file to parse")
}
file, err := os.Open(args[0])
if err != nil {
return err
}
m, err := godotenv.Parse(file)
if err != nil {
return err
}
ds, err := datastore.NewDataStore()
if err != nil {
fmt.Printf("Error: %e", err)
}
defer ds.Close()
for k, v := range m {
err = ds.SetValue(k, v, nil, envs)
if err != nil {
return err
}
}
return nil
},
}
func init() {
importFlags.envName = importCmd.Flags().StringP("environment", "e", "", "environments name")
err := importCmd.RegisterFlagCompletionFunc("environment", validEnvironmentNamesComplete)
if err != nil {
panic(err)
}
}

79
cmd/init.go Normal file
View file

@ -0,0 +1,79 @@
package cmd
import (
"fmt"
"strings"
"git.dayanhub.com/sagi/envoid/internal/datastore"
"git.dayanhub.com/sagi/envoid/internal/prompt"
"git.dayanhub.com/sagi/envoid/internal/variables"
"github.com/spf13/cobra"
)
var initCmd = &cobra.Command{
Use: "init",
Short: "creates a new project for this path. default env is set to `local`",
Long: "",
RunE: func(cmd *cobra.Command, args []string) error {
_, err := configuration.GetProject(workingDir)
if err == nil {
fmt.Printf("Project already exists. please remove if you wish to override\n")
return nil
}
ds, err := datastore.NewDataStore()
if err != nil {
return err
}
defer ds.Close()
var name string
var pass string
var envNames []string
//check if we are importing an existing file
if ds.DoesFileExists() {
fmt.Printf("Project was not found locally. But found %s file in path.\n", variables.DBFileName)
fmt.Printf("Importing Project for %s\n", workingDir)
name = prompt.StringPrompt("Project Name:")
pass = prompt.PasswordPrompt("Project Password (Same password that was used originally):")
envNames, err = ds.ListEnvironments()
if err != nil {
return err
}
fmt.Printf("Found %d environments. importing %v\n", len(envNames), envNames)
} else {
// New Project
fmt.Printf("Creating new Project @ %s\n", workingDir)
name = prompt.StringPrompt("Project Name:")
pass = prompt.PasswordPrompt("Project Password:")
envs := strings.TrimSpace(prompt.StringPrompt("Please provide environment names separated by `,`. (stage,prod):"))
if len(envs) == 0 {
envs = "local"
}
envNames = strings.Split(envs, ",")
}
project, err = configuration.NewProject(name, workingDir, pass)
if err != nil {
return err
}
for _, eName := range envNames {
if err := project.NewEnv(eName); err != nil {
return err
}
if err := ds.CreateEnv(eName); err != nil {
return err
}
}
configuration.Save()
fmt.Println("✅ Done")
return nil
},
}
func init() {
}

81
cmd/printenv.go Normal file
View file

@ -0,0 +1,81 @@
package cmd
import (
"fmt"
"strings"
"git.dayanhub.com/sagi/envoid/internal/datastore"
intErrors "git.dayanhub.com/sagi/envoid/internal/errors"
"github.com/spf13/cobra"
)
type printenvCmdFlags struct {
envName *string
}
var printenvFlags = printenvCmdFlags{}
var printenvCmd = &cobra.Command{
Use: "printenv",
Short: "prints the whole environment in a .env format",
Long: "",
RunE: func(cmd *cobra.Command, args []string) error {
err := initProject()
if err != nil {
return err
}
if project.IsEmpty() {
return intErrors.NewProjectEmptyError(project.Name)
}
if len(args) != 0 {
return intErrors.NewInvalidCommandError("expected 0 args.")
}
err = checkAmbiguousEnv(*printenvFlags.envName)
if err != nil {
return err
}
env := project.Environments[0]
if len(*printenvFlags.envName) > 0 {
e, err := project.GetEnv(*printenvFlags.envName)
if err != nil {
return err
}
env = e
}
datastore, err := datastore.NewDataStore()
if err != nil {
fmt.Printf("Error: %e", err)
}
defer datastore.Close()
vars, err := datastore.GetAll(env.Name)
if err != nil {
return err
}
for _, v := range vars {
if v.Encrypted {
fmt.Println("# SENSITIVE VAR BELOW")
}
if len(v.Value) != len(strings.ReplaceAll(v.Value, " ", "")) {
// value contain spaces. need to wrap with ""
fmt.Printf("%s=\"%s\"\n", v.Key, v.Value)
} else {
fmt.Printf("%s=%s\n", v.Key, v.Value)
}
if v.Encrypted {
fmt.Println("###")
}
}
return nil
},
}
func init() {
printenvFlags.envName = printenvCmd.Flags().StringP("environment", "e", "", "environments name")
err := printenvCmd.RegisterFlagCompletionFunc("environment", validEnvironmentNamesComplete)
if err != nil {
panic(err)
}
}

57
cmd/project.go Normal file
View file

@ -0,0 +1,57 @@
package cmd
import (
"fmt"
"github.com/spf13/cobra"
)
type projectCmdFlags struct {
projectPath *string
}
var projectFlags = projectCmdFlags{}
var projectCmd = &cobra.Command{
Use: "project",
Short: "manage project",
Long: "",
}
var lsProjectCmd = &cobra.Command{
Use: "ls",
Short: "list all projects for this user",
RunE: func(cmd *cobra.Command, args []string) error {
for _, proj := range configuration.Projects {
fmt.Printf("%s\t%s\n", proj.Name, proj.Path)
}
return nil
},
}
var rmProjectCmd = &cobra.Command{
Use: "rm",
Short: "remove a project definition. the `.envoid` file will not be removed",
RunE: func(cmd *cobra.Command, args []string) error {
project, err := configuration.GetProject(*projectFlags.projectPath)
if err != nil {
return err
}
configuration.RemoveProject(project)
configuration.Save()
return nil
},
}
func init() {
projectFlags.projectPath = rmProjectCmd.Flags().StringP("project-path", "p", "", "project path to remove")
err := rmProjectCmd.MarkFlagRequired("project-path")
if err != nil {
panic(err)
}
err = rmProjectCmd.RegisterFlagCompletionFunc("project-path", validProjectPathComplete)
if err != nil {
panic(err)
}
projectCmd.AddCommand(lsProjectCmd)
projectCmd.AddCommand(rmProjectCmd)
}

62
cmd/rm.go Normal file
View file

@ -0,0 +1,62 @@
package cmd
import (
"fmt"
"git.dayanhub.com/sagi/envoid/internal/datastore"
"git.dayanhub.com/sagi/envoid/internal/errors"
"git.dayanhub.com/sagi/envoid/internal/types"
"github.com/spf13/cobra"
)
type rmCmdFlags struct {
envName *string
}
var rmFlags = rmCmdFlags{}
var rmCmd = &cobra.Command{
Use: "rm <key>",
Short: "removes a variable from environment(s)",
Long: "",
RunE: func(cmd *cobra.Command, args []string) error {
err := initProject()
if err != nil {
return err
}
if project.IsEmpty() {
return errors.NewProjectEmptyError(project.Name)
}
if len(args) != 1 {
return errors.NewInvalidCommandError("expected 1 args. <key>")
}
key := args[0]
ds, err := datastore.NewDataStore()
if err != nil {
fmt.Printf("Error: %e", err)
}
defer ds.Close()
envs := project.Environments
if len(*rmFlags.envName) != 0 {
e, err := project.GetEnv(*rmFlags.envName)
if err != nil {
return err
}
envs = []*types.Environment{e}
}
ds.RemoveVar(key, envs)
return nil
},
}
func init() {
rmFlags.envName = rmCmd.Flags().StringP("environment", "e", "", "environments name")
err := rmCmd.RegisterFlagCompletionFunc("environment", validEnvironmentNamesComplete)
if err != nil {
panic(err)
}
}

99
cmd/root.go Normal file
View file

@ -0,0 +1,99 @@
package cmd
import (
"fmt"
"os"
"git.dayanhub.com/sagi/envoid/internal/config"
"git.dayanhub.com/sagi/envoid/internal/types"
"git.dayanhub.com/sagi/envoid/internal/variables"
"github.com/spf13/cobra"
"github.com/spf13/cobra/doc"
)
var configuration *config.Config = nil
var workingDir string
var project *types.Project = nil
var rootCmd = &cobra.Command{
Use: "envoid [command]",
Short: "envoid is an easy to use .env manager for personal (non production) use",
Long: `envoid is an easy to use .env manager for personal (non production) use.
envoid works offline and creates different encrypted environments for each project. It's mainly used to store,encrypt (with a passphrase) and share
.env variables`,
Version: fmt.Sprintf("%s commit %s", variables.Version, variables.Commit),
}
var docCmd = &cobra.Command{
Use: "doc",
Hidden: true,
RunE: func(cmd *cobra.Command, args []string) error {
err := doc.GenMarkdownTree(rootCmd, "./docs")
if err != nil {
return err
}
return nil
},
}
func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
func checkAmbiguousEnv(envName string) error {
if len(envName) == 0 && len(project.Environments) > 1 {
return fmt.Errorf("You have more than 1 environment. please provide environment name")
}
return nil
}
func initProject() error {
p, err := configuration.GetProject(workingDir)
if err != nil {
return err
}
project = p
return nil
}
func validEnvironmentNamesComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
envs := []string{}
if err := initProject(); err == nil {
for _, e := range project.Environments {
envs = append(envs, e.Name)
}
}
return envs, cobra.ShellCompDirectiveDefault
}
func validProjectPathComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
paths := []string{}
for _, p := range configuration.Projects {
paths = append(paths, fmt.Sprintf("%s\t%s", p.Path, p.Name))
}
return paths, cobra.ShellCompDirectiveDefault
}
func init() {
rootCmd.AddCommand(docCmd)
rootCmd.AddCommand(initCmd)
rootCmd.AddCommand(setCmd)
rootCmd.AddCommand(rmCmd)
rootCmd.AddCommand(getCmd)
rootCmd.AddCommand(envCmd)
rootCmd.AddCommand(printenvCmd)
rootCmd.AddCommand(importCmd)
rootCmd.AddCommand(projectCmd)
configuration = config.GetConfig()
pwd, err := os.Getwd()
if err != nil {
fmt.Printf("[error] %s. please run 'ssecret init'\n", err.Error())
os.Exit(1)
}
workingDir = pwd
}

125
cmd/set.go Normal file
View file

@ -0,0 +1,125 @@
package cmd
import (
"fmt"
"git.dayanhub.com/sagi/envoid/internal/common"
"git.dayanhub.com/sagi/envoid/internal/datastore"
"git.dayanhub.com/sagi/envoid/internal/errors"
"git.dayanhub.com/sagi/envoid/internal/types"
"github.com/spf13/cobra"
)
type setCmdFlags struct {
envName *string
encrypt *bool
}
var setFlags = setCmdFlags{}
var setCmd = &cobra.Command{
Use: "set [flags] <key> <value>",
Short: "sets a variable in environment(s)",
Long: "",
Args: func(cmd *cobra.Command, args []string) error {
if len(args) != 2 {
return errors.NewInvalidCommandError("expected 2 args. <key> <value>")
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
err := initProject()
if err != nil {
return err
}
if project.IsEmpty() {
return errors.NewProjectEmptyError(project.Name)
}
key := args[0]
val := args[1]
ds, err := datastore.NewDataStore()
if err != nil {
fmt.Printf("Error: %e", err)
}
defer ds.Close()
envs := project.Environments
if len(*setFlags.envName) != 0 {
e, err := project.GetEnv(*setFlags.envName)
if err != nil {
return err
}
envs = []*types.Environment{e}
}
err = ds.SetValue(key, val, setFlags.encrypt, envs)
if err != nil {
fmt.Printf("Error: %s\n", err.Error())
}
return nil
},
}
var setEncryptCmd = &cobra.Command{
Use: "encrypt <key>",
Short: "encrypts an existing variable in environment(s)",
Long: "",
Args: func(cmd *cobra.Command, args []string) error {
if len(args) != 1 {
return errors.NewInvalidCommandError("expected 1 args. <key>")
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
err := initProject()
if err != nil {
return err
}
if project.IsEmpty() {
return errors.NewProjectEmptyError(project.Name)
}
key := args[0]
ds, err := datastore.NewDataStore()
if err != nil {
fmt.Printf("Error: %e", err)
}
defer ds.Close()
envs := project.Environments
if len(*setFlags.envName) != 0 {
e, err := project.GetEnv(*setFlags.envName)
if err != nil {
return err
}
envs = []*types.Environment{e}
}
for _, env := range envs {
// Get value
v, err := ds.GetVar(env.Name, key)
if err != nil {
return err
}
if !v.Encrypted {
err := ds.SetValue(v.Key, v.Value, common.BoolP(true), []*types.Environment{env})
if err != nil {
return err
}
}
}
return nil
},
}
func init() {
setFlags.envName = setCmd.PersistentFlags().StringP("environment", "e", "", "environments name")
setFlags.encrypt = setCmd.Flags().BoolP("secret", "s", false, "value is a secret. encrypt this value")
err := setCmd.RegisterFlagCompletionFunc("environment", validEnvironmentNamesComplete)
if err != nil {
panic(err)
}
setCmd.AddCommand(setEncryptCmd)
}

30
docs/envoid.md Normal file
View file

@ -0,0 +1,30 @@
## envoid
envoid is an easy to use .env manager for personal (non production) use
### Synopsis
envoid is an easy to use .env manager for personal (non production) use.
envoid works offline and creates different encrypted environments for each project. It's mainly used to store,encrypt (with a passphrase) and share
.env variables
### Options
```
-h, --help help for envoid
```
### SEE ALSO
* [envoid completion](envoid_completion.md) - Generate the autocompletion script for the specified shell
* [envoid env](envoid_env.md) - manage environments
* [envoid get](envoid_get.md) - Gets a variable
* [envoid import](envoid_import.md) - import a .env file into environment(s)
* [envoid init](envoid_init.md) - creates a new project for this path. default env is set to `local`
* [envoid printenv](envoid_printenv.md) - prints the whole environment in a .env format
* [envoid project](envoid_project.md) - manage project
* [envoid rm](envoid_rm.md) - removes a variable from environment(s)
* [envoid set](envoid_set.md) - sets a variable in environment(s)
###### Auto generated by spf13/cobra on 12-Dec-2024

25
docs/envoid_completion.md Normal file
View file

@ -0,0 +1,25 @@
## envoid completion
Generate the autocompletion script for the specified shell
### Synopsis
Generate the autocompletion script for envoid for the specified shell.
See each sub-command's help for details on how to use the generated script.
### Options
```
-h, --help help for completion
```
### SEE ALSO
* [envoid](envoid.md) - envoid is an easy to use .env manager for personal (non production) use
* [envoid completion bash](envoid_completion_bash.md) - Generate the autocompletion script for bash
* [envoid completion fish](envoid_completion_fish.md) - Generate the autocompletion script for fish
* [envoid completion powershell](envoid_completion_powershell.md) - Generate the autocompletion script for powershell
* [envoid completion zsh](envoid_completion_zsh.md) - Generate the autocompletion script for zsh
###### Auto generated by spf13/cobra on 12-Dec-2024

View file

@ -0,0 +1,44 @@
## envoid completion bash
Generate the autocompletion script for bash
### Synopsis
Generate the autocompletion script for the bash shell.
This script depends on the 'bash-completion' package.
If it is not installed already, you can install it via your OS's package manager.
To load completions in your current shell session:
source <(envoid completion bash)
To load completions for every new session, execute once:
#### Linux:
envoid completion bash > /etc/bash_completion.d/envoid
#### macOS:
envoid completion bash > $(brew --prefix)/etc/bash_completion.d/envoid
You will need to start a new shell for this setup to take effect.
```
envoid completion bash
```
### Options
```
-h, --help help for bash
--no-descriptions disable completion descriptions
```
### SEE ALSO
* [envoid completion](envoid_completion.md) - Generate the autocompletion script for the specified shell
###### Auto generated by spf13/cobra on 12-Dec-2024

View file

@ -0,0 +1,35 @@
## envoid completion fish
Generate the autocompletion script for fish
### Synopsis
Generate the autocompletion script for the fish shell.
To load completions in your current shell session:
envoid completion fish | source
To load completions for every new session, execute once:
envoid completion fish > ~/.config/fish/completions/envoid.fish
You will need to start a new shell for this setup to take effect.
```
envoid completion fish [flags]
```
### Options
```
-h, --help help for fish
--no-descriptions disable completion descriptions
```
### SEE ALSO
* [envoid completion](envoid_completion.md) - Generate the autocompletion script for the specified shell
###### Auto generated by spf13/cobra on 12-Dec-2024

View file

@ -0,0 +1,32 @@
## envoid completion powershell
Generate the autocompletion script for powershell
### Synopsis
Generate the autocompletion script for powershell.
To load completions in your current shell session:
envoid completion powershell | Out-String | Invoke-Expression
To load completions for every new session, add the output of the above command
to your powershell profile.
```
envoid completion powershell [flags]
```
### Options
```
-h, --help help for powershell
--no-descriptions disable completion descriptions
```
### SEE ALSO
* [envoid completion](envoid_completion.md) - Generate the autocompletion script for the specified shell
###### Auto generated by spf13/cobra on 12-Dec-2024

View file

@ -0,0 +1,46 @@
## envoid completion zsh
Generate the autocompletion script for zsh
### Synopsis
Generate the autocompletion script for the zsh shell.
If shell completion is not already enabled in your environment you will need
to enable it. You can execute the following once:
echo "autoload -U compinit; compinit" >> ~/.zshrc
To load completions in your current shell session:
source <(envoid completion zsh)
To load completions for every new session, execute once:
#### Linux:
envoid completion zsh > "${fpath[1]}/_envoid"
#### macOS:
envoid completion zsh > $(brew --prefix)/share/zsh/site-functions/_envoid
You will need to start a new shell for this setup to take effect.
```
envoid completion zsh [flags]
```
### Options
```
-h, --help help for zsh
--no-descriptions disable completion descriptions
```
### SEE ALSO
* [envoid completion](envoid_completion.md) - Generate the autocompletion script for the specified shell
###### Auto generated by spf13/cobra on 12-Dec-2024

18
docs/envoid_env.md Normal file
View file

@ -0,0 +1,18 @@
## envoid env
manage environments
### Options
```
-h, --help help for env
```
### SEE ALSO
* [envoid](envoid.md) - envoid is an easy to use .env manager for personal (non production) use
* [envoid env add](envoid_env_add.md) - adds an environment
* [envoid env ls](envoid_env_ls.md) - list all environments in this project
* [envoid env rm](envoid_env_rm.md) - removes an environment
###### Auto generated by spf13/cobra on 12-Dec-2024

21
docs/envoid_env_add.md Normal file
View file

@ -0,0 +1,21 @@
## envoid env add
adds an environment
```
envoid env add [flags]
```
### Options
```
-b, --base-environment string base environment name (copy data from base)
-e, --environment string environment name
-h, --help help for add
```
### SEE ALSO
* [envoid env](envoid_env.md) - manage environments
###### Auto generated by spf13/cobra on 12-Dec-2024

19
docs/envoid_env_ls.md Normal file
View file

@ -0,0 +1,19 @@
## envoid env ls
list all environments in this project
```
envoid env ls [flags]
```
### Options
```
-h, --help help for ls
```
### SEE ALSO
* [envoid env](envoid_env.md) - manage environments
###### Auto generated by spf13/cobra on 12-Dec-2024

20
docs/envoid_env_rm.md Normal file
View file

@ -0,0 +1,20 @@
## envoid env rm
removes an environment
```
envoid env rm [flags]
```
### Options
```
-e, --environment string environment name
-h, --help help for rm
```
### SEE ALSO
* [envoid env](envoid_env.md) - manage environments
###### Auto generated by spf13/cobra on 12-Dec-2024

20
docs/envoid_get.md Normal file
View file

@ -0,0 +1,20 @@
## envoid get
Gets a variable
```
envoid get <key> [flags]
```
### Options
```
-e, --environment string environment name
-h, --help help for get
```
### SEE ALSO
* [envoid](envoid.md) - envoid is an easy to use .env manager for personal (non production) use
###### Auto generated by spf13/cobra on 12-Dec-2024

24
docs/envoid_import.md Normal file
View file

@ -0,0 +1,24 @@
## envoid import
import a .env file into environment(s)
### Synopsis
This will not encrypt any value. You can then use `set encrypt KEY_NAME` to encrypt
```
envoid import <file> [flags]
```
### Options
```
-e, --environment string environments name
-h, --help help for import
```
### SEE ALSO
* [envoid](envoid.md) - envoid is an easy to use .env manager for personal (non production) use
###### Auto generated by spf13/cobra on 12-Dec-2024

19
docs/envoid_init.md Normal file
View file

@ -0,0 +1,19 @@
## envoid init
creates a new project for this path. default env is set to `local`
```
envoid init [flags]
```
### Options
```
-h, --help help for init
```
### SEE ALSO
* [envoid](envoid.md) - envoid is an easy to use .env manager for personal (non production) use
###### Auto generated by spf13/cobra on 12-Dec-2024

20
docs/envoid_printenv.md Normal file
View file

@ -0,0 +1,20 @@
## envoid printenv
prints the whole environment in a .env format
```
envoid printenv [flags]
```
### Options
```
-e, --environment string environments name
-h, --help help for printenv
```
### SEE ALSO
* [envoid](envoid.md) - envoid is an easy to use .env manager for personal (non production) use
###### Auto generated by spf13/cobra on 12-Dec-2024

17
docs/envoid_project.md Normal file
View file

@ -0,0 +1,17 @@
## envoid project
manage project
### Options
```
-h, --help help for project
```
### SEE ALSO
* [envoid](envoid.md) - envoid is an easy to use .env manager for personal (non production) use
* [envoid project ls](envoid_project_ls.md) - list all projects for this user
* [envoid project rm](envoid_project_rm.md) - remove a project definition. the `.envoid` file will not be removed
###### Auto generated by spf13/cobra on 12-Dec-2024

19
docs/envoid_project_ls.md Normal file
View file

@ -0,0 +1,19 @@
## envoid project ls
list all projects for this user
```
envoid project ls [flags]
```
### Options
```
-h, --help help for ls
```
### SEE ALSO
* [envoid project](envoid_project.md) - manage project
###### Auto generated by spf13/cobra on 12-Dec-2024

20
docs/envoid_project_rm.md Normal file
View file

@ -0,0 +1,20 @@
## envoid project rm
remove a project definition. the `.envoid` file will not be removed
```
envoid project rm [flags]
```
### Options
```
-h, --help help for rm
-p, --project-path string project path to remove
```
### SEE ALSO
* [envoid project](envoid_project.md) - manage project
###### Auto generated by spf13/cobra on 12-Dec-2024

20
docs/envoid_rm.md Normal file
View file

@ -0,0 +1,20 @@
## envoid rm
removes a variable from environment(s)
```
envoid rm <key> [flags]
```
### Options
```
-e, --environment string environments name
-h, --help help for rm
```
### SEE ALSO
* [envoid](envoid.md) - envoid is an easy to use .env manager for personal (non production) use
###### Auto generated by spf13/cobra on 12-Dec-2024

22
docs/envoid_set.md Normal file
View file

@ -0,0 +1,22 @@
## envoid set
sets a variable in environment(s)
```
envoid set [flags] <key> <value>
```
### Options
```
-e, --environment string environments name
-h, --help help for set
-s, --secret value is a secret. encrypt this value
```
### SEE ALSO
* [envoid](envoid.md) - envoid is an easy to use .env manager for personal (non production) use
* [envoid set encrypt](envoid_set_encrypt.md) - encrypts an existing variable in environment(s)
###### Auto generated by spf13/cobra on 12-Dec-2024

View file

@ -0,0 +1,25 @@
## envoid set encrypt
encrypts an existing variable in environment(s)
```
envoid set encrypt <key> [flags]
```
### Options
```
-h, --help help for encrypt
```
### Options inherited from parent commands
```
-e, --environment string environments name
```
### SEE ALSO
* [envoid set](envoid_set.md) - sets a variable in environment(s)
###### Auto generated by spf13/cobra on 12-Dec-2024

30
go.mod Normal file
View file

@ -0,0 +1,30 @@
module git.dayanhub.com/sagi/envoid
go 1.23.3
require (
github.com/creasty/defaults v1.8.0
github.com/glebarez/go-sqlite v1.22.0
github.com/joho/godotenv v1.5.1
github.com/spf13/cobra v1.8.1
golang.org/x/crypto v0.29.0
golang.org/x/sync v0.9.0
golang.org/x/term v0.26.0
)
require (
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/google/uuid v1.5.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
golang.org/x/sys v0.27.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.37.6 // indirect
modernc.org/mathutil v1.6.0 // indirect
modernc.org/memory v1.7.2 // indirect
modernc.org/sqlite v1.28.0 // indirect
)

47
go.sum Normal file
View file

@ -0,0 +1,47 @@
github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creasty/defaults v1.8.0 h1:z27FJxCAa0JKt3utc0sCImAEb+spPucmKoOdLHvHYKk=
github.com/creasty/defaults v1.8.0/go.mod h1:iGzKe6pbEHnpMPtfDXZEr0NVxWnPTjb1bbDy08fPzYM=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU=
golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
modernc.org/libc v1.37.6 h1:orZH3c5wmhIQFTXF+Nt+eeauyd+ZIt2BX6ARe+kD+aw=
modernc.org/libc v1.37.6/go.mod h1:YAXkAZ8ktnkCKaN9sw/UDeUVkGYJ/YquGO4FTi5nmHE=
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E=
modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E=
modernc.org/sqlite v1.28.0 h1:Zx+LyDDmXczNnEQdvPuEfcFVA2ZPyaD7UCZDjef3BHQ=
modernc.org/sqlite v1.28.0/go.mod h1:Qxpazz0zH8Z1xCFyi5GSL3FzbtZ3fvbjmywNogldEW0=

View file

@ -0,0 +1,9 @@
package common
func BoolP(val bool) *bool {
return &val
}
func StringP(val string) *string {
return &val
}

View file

@ -0,0 +1,10 @@
package common
import "strings"
func StrToSnakeCase(s string) string {
snake := strings.ToLower(s)
snake = strings.ReplaceAll(snake, " ", "_")
snake = strings.ReplaceAll(snake, "\t", "_")
return snake
}

145
internal/config/config.go Normal file
View file

@ -0,0 +1,145 @@
package config
import (
"encoding/base64"
"encoding/json"
"fmt"
"os"
"path"
"git.dayanhub.com/sagi/envoid/internal/errors"
"git.dayanhub.com/sagi/envoid/internal/types"
"github.com/creasty/defaults"
)
type Config struct {
PWD string `json:"-"`
Projects []*types.Project `json:"projects" default:"[]"`
}
var configPath string
var configStruct *Config
func (c *Config) GetProject(projectPath string) (*types.Project, error) {
var p *types.Project
for _, project := range configStruct.Projects {
if project.Path == projectPath {
p = project
break
}
}
if p == nil {
return nil, errors.NewProjectNotFoundError(projectPath)
}
return p, nil
}
func (c *Config) RemoveProject(project *types.Project) {
projects := []*types.Project{}
for _, p := range c.Projects {
if p.Name != project.Name && p.Path != project.Path {
projects = append(projects, p)
}
}
c.Projects = projects
}
func (c *Config) NewProject(name string, path string, password string) (*types.Project, error) {
encPass := base64.RawStdEncoding.EncodeToString([]byte(password))
p := &types.Project{
Name: name,
Path: path,
Password: encPass,
Environments: []*types.Environment{},
}
configStruct.Projects = append(configStruct.Projects, p)
SaveConfig()
return p, nil
}
func GetConfig() *Config {
return configStruct
}
func (c *Config) Save() {
SaveConfig()
}
func CreateConfigIfNotExists() error {
return nil
}
func init() {
userConfigDir, err := os.UserConfigDir()
configDir := path.Join(userConfigDir, "envoid")
configPath = path.Join(configDir, "config.json")
if err != nil {
fmt.Printf("[ERROR] Failed to fetch user config directory. %e\n", err)
os.Exit(1)
}
if _, err := os.Stat(configDir); os.IsNotExist(err) {
err := os.MkdirAll(configDir, 0700)
if err != nil {
panic(err)
}
}
var configFile *os.File
if _, err := os.Stat(configPath); os.IsNotExist(err) {
configFile, err = os.Create(configPath)
defer func() {
err = configFile.Close()
if err != nil {
panic(err)
}
}()
if err != nil {
fmt.Printf("[ERROR] Failed to create config file @ %s. %e\n", configPath, err)
os.Exit(1)
}
}
configStruct, err = loadConfig()
if err != nil {
fmt.Printf("[ERROR] Failed to load config file @ %s. %e\n", configPath, err)
os.Exit(1)
}
}
func loadConfig() (*Config, error) {
c := &Config{}
err := defaults.Set(c)
if err != nil {
panic(err)
}
file, err := os.ReadFile(configPath)
if err != nil {
return nil, err
}
if len(file) == 0 {
return c, nil
}
if err = json.Unmarshal(file, c); err != nil {
return nil, err
}
pwd, err := os.Getwd()
if err != nil {
return nil, err
}
c.PWD = pwd
return c, nil
}
func SaveConfig() {
yml, err := json.Marshal(configStruct)
if err != nil {
fmt.Printf("[ERROR] Failed to convert config to json. %e\n", err)
os.Exit(1)
}
err = os.WriteFile(configPath, yml, 0600)
if err != nil {
fmt.Printf("[ERROR] Failed to save config file @ %s. %e\n", configPath, err)
os.Exit(1)
}
}

View file

@ -0,0 +1,223 @@
package datastore
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"errors"
"fmt"
"os"
"sort"
"git.dayanhub.com/sagi/envoid/internal/common"
"git.dayanhub.com/sagi/envoid/internal/config"
intErrors "git.dayanhub.com/sagi/envoid/internal/errors"
"git.dayanhub.com/sagi/envoid/internal/types"
"git.dayanhub.com/sagi/envoid/internal/variables"
"golang.org/x/crypto/scrypt"
"golang.org/x/sync/errgroup"
)
type datastore struct {
db *db
}
func NewDataStore() (*datastore, error) {
db, err := newDB()
if err != nil {
return nil, err
}
return &datastore{
db: db,
}, nil
}
func (d *datastore) CreateEnv(name string) error {
table_name := envNameToTableName(name)
return d.db.createTableIfNotExists(table_name)
}
func (d *datastore) DoesFileExists() bool {
pwd := config.GetConfig().PWD
filePath := fmt.Sprintf("%s/%s", pwd, variables.DBFileName)
if _, err := os.Stat(filePath); errors.Is(err, os.ErrNotExist) {
return false
}
return true
}
func (d *datastore) ListEnvironments() ([]string, error) {
return d.db.listTables()
}
func (d *datastore) CreateEnvOffExsisting(new_env string, base_env string) error {
table_name_new := envNameToTableName(new_env)
table_name_base := envNameToTableName(base_env)
err := d.CreateEnv(new_env)
if err != nil {
return err
}
err = d.db.copyContentFromTo(table_name_base, table_name_new)
if err != nil {
return err
}
return nil
}
func (d *datastore) Close() error {
return d.db.close()
}
func (d *datastore) SetValue(key string, value string, encrypted *bool, envs []*types.Environment) error {
if encrypted == nil {
encrypted = common.BoolP(false)
}
if *encrypted {
v, err := enc(value)
if err != nil {
return err
}
value = *v
}
for _, env := range envs {
table_name := envNameToTableName(env.Name)
if err := d.db.setVar(table_name, key, value, *encrypted); err != nil {
return err
}
}
return nil
}
func (d *datastore) GetAll(envName string) ([]*types.EnvVar, error) {
table_name := envNameToTableName(envName)
vars, err := d.db.getAll(table_name)
if err != nil {
return vars, err
}
g := new(errgroup.Group)
for _, v := range vars {
g.Go(func() error {
if v.Encrypted {
if v.Value, err = dec(v.Value); err != nil {
return &intErrors.InvalidPasswordError{}
}
}
return nil
})
}
if err := g.Wait(); err != nil {
return vars, err
}
sort.SliceStable(vars, func(i, j int) bool {
return vars[i].Key < vars[j].Key
})
return vars, nil
}
func (d *datastore) RemoveVar(key string, envs []*types.Environment) {
for _, env := range envs {
table_name := envNameToTableName(env.Name)
d.db.rmVar(table_name, key)
}
}
func (d *datastore) GetVar(envName string, key string) (*types.EnvVar, error) {
table_name := envNameToTableName(envName)
v, err := d.db.getVar(table_name, key)
if err != nil {
return v, err
}
if v.Encrypted {
if v.Value, err = dec(v.Value); err != nil {
return v, &intErrors.InvalidPasswordError{}
}
}
return v, err
}
func (d *datastore) RemoveEnv(envName string) error {
table_name := envNameToTableName(envName)
return d.db.deleteTable(table_name)
}
func enc(s string) (*string, error) {
conf := config.GetConfig()
proj, _ := conf.GetProject(conf.PWD)
key, salt, err := deriveKey([]byte(proj.Password), nil)
data := []byte(s)
if err != nil {
return nil, err
}
blockCipher, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(blockCipher)
if err != nil {
return nil, err
}
nonce := make([]byte, gcm.NonceSize())
if _, err = rand.Read(nonce); err != nil {
return nil, err
}
ciphertext := gcm.Seal(nonce, nonce, data, nil)
ciphertext = append(ciphertext, salt...)
str := string(ciphertext)
return &str, nil
}
func dec(s string) (string, error) {
data := []byte(s)
salt, data := data[len(data)-32:], data[:len(data)-32]
conf := config.GetConfig()
proj, _ := conf.GetProject(conf.PWD)
key, _, err := deriveKey([]byte(proj.Password), salt)
if err != nil {
return "", err
}
blockCipher, err := aes.NewCipher(key)
if err != nil {
return "", err
}
gcm, err := cipher.NewGCM(blockCipher)
if err != nil {
return "", err
}
nonce, ciphertext := data[:gcm.NonceSize()], data[gcm.NonceSize():]
plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
if err != nil {
return "", err
}
str := string(plaintext)
return str, nil
}
func deriveKey(password, salt []byte) ([]byte, []byte, error) {
if salt == nil {
salt = make([]byte, 32)
if _, err := rand.Read(salt); err != nil {
return nil, nil, err
}
}
key, err := scrypt.Key(password, salt, 1048576, 8, 1, 32)
if err != nil {
return nil, nil, err
}
return key, salt, nil
}

135
internal/datastore/db.go Normal file
View file

@ -0,0 +1,135 @@
package datastore
import (
"fmt"
"database/sql"
"git.dayanhub.com/sagi/envoid/internal/common"
"git.dayanhub.com/sagi/envoid/internal/errors"
"git.dayanhub.com/sagi/envoid/internal/types"
"git.dayanhub.com/sagi/envoid/internal/variables"
_ "github.com/glebarez/go-sqlite"
)
type db struct {
con *sql.DB
}
func newDB() (*db, error) {
con, err := sql.Open("sqlite", variables.DBFileName)
if err != nil {
fmt.Printf("%v\n", err)
return nil, err
}
return &db{
con: con,
}, nil
}
func (d *db) close() error {
return d.con.Close()
}
func (d *db) createTableIfNotExists(table_name string) error {
_, err := d.con.Exec(fmt.Sprintf("CREATE TABLE IF NOT EXISTS %s (key TEXT PRIMARY KEY NOT NULL, value BLOB NOT NULL, encrypted BOOL NOT NULL);", table_name))
if err != nil {
return err
}
return nil
}
func (d *db) listTables() ([]string, error) {
tables := []string{}
q := "SELECT name FROM sqlite_schema WHERE type ='table' AND name NOT LIKE 'sqlite_%' AND name LIKE ?"
rows, err := d.con.Query(q, fmt.Sprintf("%s_%%", variables.DBTablePrefix))
if err != nil {
return tables, err
}
for rows.Next() {
var table string
if err := rows.Scan(&table); err != nil {
return tables, err
}
tables = append(tables, tableNameToEnvName(table))
}
return tables, nil
}
func (d *db) copyContentFromTo(table_name_target string, table_name_dest string) error {
_, err := d.con.Exec(fmt.Sprintf("INSERT INTO %s (key, value, encrypted) SELECT * FROM %s", table_name_dest, table_name_target))
if err != nil {
return err
}
return nil
}
func (d *db) deleteTable(table_name string) error {
_, err := d.con.Exec(fmt.Sprintf("DROP TABLE IF EXISTS %s", table_name))
return err
}
func (d *db) getVar(table_name string, key string) (*types.EnvVar, error) {
q := fmt.Sprintf("SELECT * FROM %s WHERE key=?", table_name)
row := d.con.QueryRow(q, key)
envVar := &types.EnvVar{}
err := row.Scan(&envVar.Key, &envVar.Value, &envVar.Encrypted)
if err != nil {
return envVar, errors.NewNoKeyFoundError(key)
}
return envVar, nil
}
func (d *db) getAll(table_name string) ([]*types.EnvVar, error) {
entries := []*types.EnvVar{}
rows, err := d.con.Query(fmt.Sprintf("SELECT * FROM %s", table_name))
if err != nil {
return entries, err
}
for rows.Next() {
key := ""
value := ""
enc := common.BoolP(false)
err := rows.Scan(&key, &value, enc)
if err != nil {
return entries, err
}
e := &types.EnvVar{
Key: key,
Value: value,
Encrypted: *enc,
}
entries = append(entries, e)
}
return entries, nil
}
func (d *db) setVar(table_name string, key string, value string, encrypted bool) error {
q := fmt.Sprintf("INSERT INTO %s (key, value, encrypted) values ( ? , ? , ? ) ON CONFLICT (key) DO UPDATE SET value = ?, encrypted = ?", table_name)
_, err := d.con.Exec(q, key, value, encrypted, value, encrypted)
if err != nil {
fmt.Println(err)
return errors.NewSecretExsistsError(key)
}
return nil
}
func (d *db) rmVar(table_name string, key string) {
q := fmt.Sprintf("DELETE FROM %s WHERE key = ?", table_name)
_, _ = d.con.Exec(q, key)
}
func envNameToTableName(envName string) string {
envSanke := common.StrToSnakeCase(envName)
return fmt.Sprintf("%s_%s", variables.DBTablePrefix, envSanke)
}
func tableNameToEnvName(tableName string) string {
// remove '<prefix>_'
return tableName[len(variables.DBTablePrefix)+1:]
}

View file

@ -0,0 +1,15 @@
package errors
import "fmt"
type EnvironmentExistsError struct {
name string
}
func (e *EnvironmentExistsError) Error() string {
return fmt.Sprintf("environment %s already exists", e.name)
}
func NewEnvironmentExistsError(name string) *EnvironmentExistsError {
return &EnvironmentExistsError{name: name}
}

View file

@ -0,0 +1,16 @@
package errors
import "fmt"
type EnvironmentNotFoundError struct {
path string
name string
}
func (e *EnvironmentNotFoundError) Error() string {
return fmt.Sprintf("No env '%s' definition for path %s. Did you initialize?", e.name, e.path)
}
func NewEnvironmentNotFoundError(path string, name string) *EnvironmentNotFoundError {
return &EnvironmentNotFoundError{path: path, name: name}
}

View file

@ -0,0 +1,15 @@
package errors
import "fmt"
type InvalidCommandError struct {
msg string
}
func (e *InvalidCommandError) Error() string {
return fmt.Sprintf("invalid command. %v", e.msg)
}
func NewInvalidCommandError(msg string) *InvalidCommandError {
return &InvalidCommandError{msg: msg}
}

View file

@ -0,0 +1,16 @@
package errors
import "fmt"
type InvalidFlagValueError struct {
flag string
invalidValue string
}
func (e *InvalidFlagValueError) Error() string {
return fmt.Sprintf("invalid value '%s' for flag --%s", e.invalidValue, e.flag)
}
func NewInvalidFlagValueError(flag string, invalidValue string) *InvalidFlagValueError {
return &InvalidFlagValueError{flag: flag, invalidValue: invalidValue}
}

View file

@ -0,0 +1,12 @@
package errors
type InvalidPasswordError struct {
}
func (e *InvalidPasswordError) Error() string {
return "invalid password. is your environment set correctly?"
}
func NewInvalidPasswordError() *InvalidPasswordError {
return &InvalidPasswordError{}
}

View file

@ -0,0 +1,15 @@
package errors
import "fmt"
type NoKeyFoundError struct {
key string
}
func (e *NoKeyFoundError) Error() string {
return fmt.Sprintf("key %s not found", e.key)
}
func NewNoKeyFoundError(key string) *NoKeyFoundError {
return &NoKeyFoundError{key: key}
}

View file

@ -0,0 +1,15 @@
package errors
import "fmt"
type NoConfigFoundError struct {
path string
}
func (e *NoConfigFoundError) Error() string {
return fmt.Sprintf("no config file found in %s", e.path)
}
func NewNoConfigFoundError(path string) *NoConfigFoundError {
return &NoConfigFoundError{path: path}
}

View file

@ -0,0 +1,15 @@
package errors
import "fmt"
type ProjectEmptyError struct {
name string
}
func (e *ProjectEmptyError) Error() string {
return fmt.Sprintf("Project %s does not have any environments.", e.name)
}
func NewProjectEmptyError(name string) *ProjectEmptyError {
return &ProjectEmptyError{name: name}
}

View file

@ -0,0 +1,15 @@
package errors
import "fmt"
type ProjectNotFoundError struct {
path string
}
func (e *ProjectNotFoundError) Error() string {
return fmt.Sprintf("No project found for path '%s'. Did you initialize?", e.path)
}
func NewProjectNotFoundError(path string) *ProjectNotFoundError {
return &ProjectNotFoundError{path: path}
}

View file

@ -0,0 +1,15 @@
package errors
import "fmt"
type SecretExistsError struct {
secret string
}
func (e *SecretExistsError) Error() string {
return fmt.Sprintf("secret %s already exists", e.secret)
}
func NewSecretExsistsError(secret string) *SecretExistsError {
return &SecretExistsError{secret: secret}
}

View file

@ -0,0 +1,23 @@
package prompt
import (
"fmt"
"os"
"syscall"
"golang.org/x/term"
)
func PasswordPrompt(label string) string {
var s string
for {
fmt.Fprint(os.Stderr, label+" ")
b, _ := term.ReadPassword(int(syscall.Stdin))
s = string(b)
if s != "" {
break
}
}
fmt.Println()
return s
}

21
internal/prompt/string.go Normal file
View file

@ -0,0 +1,21 @@
package prompt
import (
"bufio"
"fmt"
"os"
"strings"
)
func StringPrompt(label string) string {
var s string
r := bufio.NewReader(os.Stdin)
for {
fmt.Fprint(os.Stderr, label+" ")
s, _ = r.ReadString('\n')
if s != "" {
break
}
}
return strings.TrimSpace(s)
}

View file

@ -0,0 +1,7 @@
package types
type EnvVar struct {
Key string
Value string
Encrypted bool
}

View file

@ -0,0 +1,6 @@
package types
type Environment struct {
Name string `json:"name"`
Password string `json:"-"`
}

58
internal/types/project.go Normal file
View file

@ -0,0 +1,58 @@
package types
import (
"encoding/base64"
"fmt"
"os"
"git.dayanhub.com/sagi/envoid/internal/errors"
)
type Project struct {
Path string `json:"path"`
Name string `json:"name"`
Password string `json:"password"`
Environments []*Environment `json:"envorinments" default:"[]"`
}
func (p *Project) GetEnv(name string) (*Environment, error) {
var e *Environment
for _, env := range p.Environments {
if env.Name == name {
passByte, err := base64.RawStdEncoding.DecodeString(p.Password)
if err != nil {
fmt.Printf("[ERROR] Failed to decode project password @ %s. %e\n", p.Path, err)
os.Exit(1)
}
e = &Environment{
Name: env.Name,
Password: string(passByte),
}
}
}
if e == nil {
return nil, errors.NewEnvironmentNotFoundError(p.Path, name)
}
return e, nil
}
func (p *Project) NewEnv(name string) error {
e := &Environment{
Name: name,
}
p.Environments = append(p.Environments, e)
return nil
}
func (p *Project) RemoveEnv(name string) {
environments := []*Environment{}
for _, env := range p.Environments {
if env.Name != name {
environments = append(environments, env)
}
}
p.Environments = environments
}
func (p *Project) IsEmpty() bool {
return len(p.Environments) == 0
}

View file

@ -0,0 +1,7 @@
package variables
const (
DBFileName = ".envoid"
DBTablePrefix = "envoid"
ConfigFolder = "envoid"
)

View file

@ -0,0 +1,6 @@
package variables
var (
Commit string = "HEAD"
Version string = "development"
)

9
main.go Normal file
View file

@ -0,0 +1,9 @@
package main
import (
"git.dayanhub.com/sagi/envoid/cmd"
)
func main() {
cmd.Execute()
}

6
renovate.json Normal file
View file

@ -0,0 +1,6 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"reviewers": [
"sagi"
]
}