commit 693350fa49145143c853bde5733ca3715437e912 Author: Sagi Dayan Date: Fri Nov 22 17:29:25 2024 +0200 Initial commit Signed-off-by: Sagi Dayan diff --git a/.forgejo/workflows/codespell.yml b/.forgejo/workflows/codespell.yml new file mode 100644 index 00000000..6badd57e --- /dev/null +++ b/.forgejo/workflows/codespell.yml @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..4dd07394 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +build/ +vendor/ +# test secrets +.secrets diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..9f591702 --- /dev/null +++ b/Makefile @@ -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 + diff --git a/README.md b/README.md new file mode 100644 index 00000000..bac05906 --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +# Envoid + +##### The `.env`(s) manager you never knew you needed + +⚠ WIP ! + +[Documentation](docs/envoid.md) diff --git a/cmd/env.go b/cmd/env.go new file mode 100644 index 00000000..8f5be042 --- /dev/null +++ b/cmd/env.go @@ -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) +} diff --git a/cmd/get.go b/cmd/get.go new file mode 100644 index 00000000..9eb9b090 --- /dev/null +++ b/cmd/get.go @@ -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 ", + 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 := 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) + } +} diff --git a/cmd/import.go b/cmd/import.go new file mode 100644 index 00000000..56b7a83a --- /dev/null +++ b/cmd/import.go @@ -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 ", + 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) + } +} diff --git a/cmd/init.go b/cmd/init.go new file mode 100644 index 00000000..d3df77ea --- /dev/null +++ b/cmd/init.go @@ -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() { +} diff --git a/cmd/printenv.go b/cmd/printenv.go new file mode 100644 index 00000000..a21f6f6c --- /dev/null +++ b/cmd/printenv.go @@ -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) + } +} diff --git a/cmd/project.go b/cmd/project.go new file mode 100644 index 00000000..de5924de --- /dev/null +++ b/cmd/project.go @@ -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) +} diff --git a/cmd/rm.go b/cmd/rm.go new file mode 100644 index 00000000..05c5b214 --- /dev/null +++ b/cmd/rm.go @@ -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 ", + 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 := 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) + } +} diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 00000000..4ab2197b --- /dev/null +++ b/cmd/root.go @@ -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 +} diff --git a/cmd/set.go b/cmd/set.go new file mode 100644 index 00000000..4c7baec4 --- /dev/null +++ b/cmd/set.go @@ -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] ", + 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. ") + } + 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 ", + 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. ") + } + 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) +} diff --git a/docs/envoid.md b/docs/envoid.md new file mode 100644 index 00000000..7060f2ce --- /dev/null +++ b/docs/envoid.md @@ -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 diff --git a/docs/envoid_completion.md b/docs/envoid_completion.md new file mode 100644 index 00000000..4e823ce2 --- /dev/null +++ b/docs/envoid_completion.md @@ -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 diff --git a/docs/envoid_completion_bash.md b/docs/envoid_completion_bash.md new file mode 100644 index 00000000..b6c60345 --- /dev/null +++ b/docs/envoid_completion_bash.md @@ -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 diff --git a/docs/envoid_completion_fish.md b/docs/envoid_completion_fish.md new file mode 100644 index 00000000..54743fe9 --- /dev/null +++ b/docs/envoid_completion_fish.md @@ -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 diff --git a/docs/envoid_completion_powershell.md b/docs/envoid_completion_powershell.md new file mode 100644 index 00000000..e33c5b6c --- /dev/null +++ b/docs/envoid_completion_powershell.md @@ -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 diff --git a/docs/envoid_completion_zsh.md b/docs/envoid_completion_zsh.md new file mode 100644 index 00000000..3fbb428e --- /dev/null +++ b/docs/envoid_completion_zsh.md @@ -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 diff --git a/docs/envoid_env.md b/docs/envoid_env.md new file mode 100644 index 00000000..ccd4e3ae --- /dev/null +++ b/docs/envoid_env.md @@ -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 diff --git a/docs/envoid_env_add.md b/docs/envoid_env_add.md new file mode 100644 index 00000000..cb8cba9c --- /dev/null +++ b/docs/envoid_env_add.md @@ -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 diff --git a/docs/envoid_env_ls.md b/docs/envoid_env_ls.md new file mode 100644 index 00000000..1eaf89df --- /dev/null +++ b/docs/envoid_env_ls.md @@ -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 diff --git a/docs/envoid_env_rm.md b/docs/envoid_env_rm.md new file mode 100644 index 00000000..3a53af9f --- /dev/null +++ b/docs/envoid_env_rm.md @@ -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 diff --git a/docs/envoid_get.md b/docs/envoid_get.md new file mode 100644 index 00000000..e6ee348c --- /dev/null +++ b/docs/envoid_get.md @@ -0,0 +1,20 @@ +## envoid get + +Gets a variable + +``` +envoid get [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 diff --git a/docs/envoid_import.md b/docs/envoid_import.md new file mode 100644 index 00000000..6a3672db --- /dev/null +++ b/docs/envoid_import.md @@ -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 [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 diff --git a/docs/envoid_init.md b/docs/envoid_init.md new file mode 100644 index 00000000..ede4d132 --- /dev/null +++ b/docs/envoid_init.md @@ -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 diff --git a/docs/envoid_printenv.md b/docs/envoid_printenv.md new file mode 100644 index 00000000..7a0013ec --- /dev/null +++ b/docs/envoid_printenv.md @@ -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 diff --git a/docs/envoid_project.md b/docs/envoid_project.md new file mode 100644 index 00000000..a4b11ba1 --- /dev/null +++ b/docs/envoid_project.md @@ -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 diff --git a/docs/envoid_project_ls.md b/docs/envoid_project_ls.md new file mode 100644 index 00000000..ab0721ee --- /dev/null +++ b/docs/envoid_project_ls.md @@ -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 diff --git a/docs/envoid_project_rm.md b/docs/envoid_project_rm.md new file mode 100644 index 00000000..2d997fc5 --- /dev/null +++ b/docs/envoid_project_rm.md @@ -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 diff --git a/docs/envoid_rm.md b/docs/envoid_rm.md new file mode 100644 index 00000000..36737859 --- /dev/null +++ b/docs/envoid_rm.md @@ -0,0 +1,20 @@ +## envoid rm + +removes a variable from environment(s) + +``` +envoid rm [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 diff --git a/docs/envoid_set.md b/docs/envoid_set.md new file mode 100644 index 00000000..e9ace4fc --- /dev/null +++ b/docs/envoid_set.md @@ -0,0 +1,22 @@ +## envoid set + +sets a variable in environment(s) + +``` +envoid set [flags] +``` + +### 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 diff --git a/docs/envoid_set_encrypt.md b/docs/envoid_set_encrypt.md new file mode 100644 index 00000000..65ce51fe --- /dev/null +++ b/docs/envoid_set_encrypt.md @@ -0,0 +1,25 @@ +## envoid set encrypt + +encrypts an existing variable in environment(s) + +``` +envoid set encrypt [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 diff --git a/go.mod b/go.mod new file mode 100644 index 00000000..ff0fa345 --- /dev/null +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 00000000..4e964d93 --- /dev/null +++ b/go.sum @@ -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= diff --git a/internal/common/pointers.go b/internal/common/pointers.go new file mode 100644 index 00000000..02759e41 --- /dev/null +++ b/internal/common/pointers.go @@ -0,0 +1,9 @@ +package common + +func BoolP(val bool) *bool { + return &val +} + +func StringP(val string) *string { + return &val +} diff --git a/internal/common/str_convert.go b/internal/common/str_convert.go new file mode 100644 index 00000000..56e4dbc8 --- /dev/null +++ b/internal/common/str_convert.go @@ -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 +} diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 00000000..a81d00a0 --- /dev/null +++ b/internal/config/config.go @@ -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) + } +} diff --git a/internal/datastore/datastore.go b/internal/datastore/datastore.go new file mode 100644 index 00000000..24a47265 --- /dev/null +++ b/internal/datastore/datastore.go @@ -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 +} diff --git a/internal/datastore/db.go b/internal/datastore/db.go new file mode 100644 index 00000000..2ee2abe3 --- /dev/null +++ b/internal/datastore/db.go @@ -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 '_' + return tableName[len(variables.DBTablePrefix)+1:] +} diff --git a/internal/errors/env_already_exists.go b/internal/errors/env_already_exists.go new file mode 100644 index 00000000..7315a18b --- /dev/null +++ b/internal/errors/env_already_exists.go @@ -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} +} diff --git a/internal/errors/env_not_found.go b/internal/errors/env_not_found.go new file mode 100644 index 00000000..2d5743a1 --- /dev/null +++ b/internal/errors/env_not_found.go @@ -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} +} diff --git a/internal/errors/invalid_command.go b/internal/errors/invalid_command.go new file mode 100644 index 00000000..40688221 --- /dev/null +++ b/internal/errors/invalid_command.go @@ -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} +} diff --git a/internal/errors/invalid_flag_value.go b/internal/errors/invalid_flag_value.go new file mode 100644 index 00000000..77bc9ab1 --- /dev/null +++ b/internal/errors/invalid_flag_value.go @@ -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} +} diff --git a/internal/errors/invalid_password.go b/internal/errors/invalid_password.go new file mode 100644 index 00000000..cd713a71 --- /dev/null +++ b/internal/errors/invalid_password.go @@ -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{} +} diff --git a/internal/errors/key_not_found.go b/internal/errors/key_not_found.go new file mode 100644 index 00000000..c4f71357 --- /dev/null +++ b/internal/errors/key_not_found.go @@ -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} +} diff --git a/internal/errors/no_config_found.go b/internal/errors/no_config_found.go new file mode 100644 index 00000000..71aee7ce --- /dev/null +++ b/internal/errors/no_config_found.go @@ -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} +} diff --git a/internal/errors/project_is_empty.go b/internal/errors/project_is_empty.go new file mode 100644 index 00000000..6a0e006d --- /dev/null +++ b/internal/errors/project_is_empty.go @@ -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} +} diff --git a/internal/errors/project_not_found.go b/internal/errors/project_not_found.go new file mode 100644 index 00000000..4ee33483 --- /dev/null +++ b/internal/errors/project_not_found.go @@ -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} +} diff --git a/internal/errors/secret_exists.go b/internal/errors/secret_exists.go new file mode 100644 index 00000000..487497e7 --- /dev/null +++ b/internal/errors/secret_exists.go @@ -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} +} diff --git a/internal/prompt/password.go b/internal/prompt/password.go new file mode 100644 index 00000000..99d0f791 --- /dev/null +++ b/internal/prompt/password.go @@ -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 +} diff --git a/internal/prompt/string.go b/internal/prompt/string.go new file mode 100644 index 00000000..b7b063b9 --- /dev/null +++ b/internal/prompt/string.go @@ -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) +} diff --git a/internal/types/env_var.go b/internal/types/env_var.go new file mode 100644 index 00000000..1c279e34 --- /dev/null +++ b/internal/types/env_var.go @@ -0,0 +1,7 @@ +package types + +type EnvVar struct { + Key string + Value string + Encrypted bool +} diff --git a/internal/types/environment.go b/internal/types/environment.go new file mode 100644 index 00000000..b76be9b5 --- /dev/null +++ b/internal/types/environment.go @@ -0,0 +1,6 @@ +package types + +type Environment struct { + Name string `json:"name"` + Password string `json:"-"` +} diff --git a/internal/types/project.go b/internal/types/project.go new file mode 100644 index 00000000..6902cd10 --- /dev/null +++ b/internal/types/project.go @@ -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 +} diff --git a/internal/variables/constants.go b/internal/variables/constants.go new file mode 100644 index 00000000..49b37244 --- /dev/null +++ b/internal/variables/constants.go @@ -0,0 +1,7 @@ +package variables + +const ( + DBFileName = ".envoid" + DBTablePrefix = "envoid" + ConfigFolder = "envoid" +) diff --git a/internal/variables/variables.go b/internal/variables/variables.go new file mode 100644 index 00000000..b692a64f --- /dev/null +++ b/internal/variables/variables.go @@ -0,0 +1,6 @@ +package variables + +var ( + Commit string = "HEAD" + Version string = "development" +) diff --git a/main.go b/main.go new file mode 100644 index 00000000..299cf4fd --- /dev/null +++ b/main.go @@ -0,0 +1,9 @@ +package main + +import ( + "git.dayanhub.com/sagi/envoid/cmd" +) + +func main() { + cmd.Execute() +} diff --git a/renovate.json b/renovate.json new file mode 100644 index 00000000..c5f8d636 --- /dev/null +++ b/renovate.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "reviewers": [ + "sagi" + ] +}