0
0
forked from toolshed/abra

Compare commits

..

5 Commits

Author SHA1 Message Date
964d4efca4 fix: secret name generation when secretId is not part of the secret name 2023-12-04 14:37:41 +01:00
cb49cf06d1 chore: drop old godotenv pointers [ci skip]
Follows 9affda8a70
2023-12-02 13:02:24 +01:00
9affda8a70 chore: update godotenv fork commit pointer
Follows coop-cloud/abra#391
2023-12-02 12:59:42 +01:00
3957b7c965 proper env modifiers support
This implements proper modifier support in the env file using this new fork of the godotenv library. The modifier implementation is quite basic for but can be improved later if needed. See this commit for the actual implementation.

Because we are now using proper modifer parsing, it does not affect the parsing of value, so this is possible again:
```
MY_VAR="#foo"
```
Closes coop-cloud/organising#535
2023-12-01 11:03:52 +00:00
0d83339d80 fix(ssh): increase connection timeout #482
see coop-cloud/organising#482
2023-11-30 16:35:53 +01:00
10 changed files with 93 additions and 66 deletions

View File

@ -108,7 +108,7 @@ var appNewCommand = cli.Command{
} }
envSamplePath := path.Join(config.RECIPES_DIR, recipe.Name, ".env.sample") envSamplePath := path.Join(config.RECIPES_DIR, recipe.Name, ".env.sample")
secretsConfig, err := secret.ReadSecretsConfig(envSamplePath, composeFiles, recipe.Name) secretsConfig, err := secret.ReadSecretsConfig(envSamplePath, composeFiles, config.StackName(internal.Domain))
if err != nil { if err != nil {
return err return err
} }
@ -168,14 +168,8 @@ var appNewCommand = cli.Command{
type AppSecrets map[string]string type AppSecrets map[string]string
// createSecrets creates all secrets for a new app. // createSecrets creates all secrets for a new app.
func createSecrets(cl *dockerClient.Client, secretsConfig map[string]secret.SecretValue, sanitisedAppName string) (AppSecrets, error) { func createSecrets(cl *dockerClient.Client, secretsConfig map[string]secret.Secret, sanitisedAppName string) (AppSecrets, error) {
// NOTE(d1): trim to match app.StackName() implementation secrets, err := secret.GenerateSecrets(cl, secretsConfig, internal.NewAppServer)
if len(sanitisedAppName) > 45 {
logrus.Debugf("trimming %s to %s to avoid runtime limits", sanitisedAppName, sanitisedAppName[:45])
sanitisedAppName = sanitisedAppName[:45]
}
secrets, err := secret.GenerateSecrets(cl, secretsConfig, sanitisedAppName, internal.NewAppServer)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -217,7 +211,7 @@ func ensureDomainFlag(recipe recipe.Recipe, server string) error {
} }
// promptForSecrets asks if we should generate secrets for a new app. // promptForSecrets asks if we should generate secrets for a new app.
func promptForSecrets(recipeName string, secretsConfig map[string]secret.SecretValue) error { func promptForSecrets(recipeName string, secretsConfig map[string]secret.Secret) error {
if len(secretsConfig) == 0 { if len(secretsConfig) == 0 {
logrus.Debugf("%s has no secrets to generate, skipping...", recipeName) logrus.Debugf("%s has no secrets to generate, skipping...", recipeName)
return nil return nil

View File

@ -91,7 +91,7 @@ var appSecretGenerateCommand = cli.Command{
logrus.Fatal(err) logrus.Fatal(err)
} }
secrets, err := secret.ReadSecretsConfig(app.Path, composeFiles, app.Recipe) secrets, err := secret.ReadSecretsConfig(app.Path, composeFiles, app.StackName())
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
@ -104,7 +104,7 @@ var appSecretGenerateCommand = cli.Command{
logrus.Fatalf("%s doesn't exist in the env config?", secretName) logrus.Fatalf("%s doesn't exist in the env config?", secretName)
} }
s.Version = secretVersion s.Version = secretVersion
secrets = map[string]secret.SecretValue{ secrets = map[string]secret.Secret{
secretName: s, secretName: s,
} }
} }
@ -114,7 +114,7 @@ var appSecretGenerateCommand = cli.Command{
logrus.Fatal(err) logrus.Fatal(err)
} }
secretVals, err := secret.GenerateSecrets(cl, secrets, app.StackName(), app.Server) secretVals, err := secret.GenerateSecrets(cl, secrets, app.Server)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
@ -274,7 +274,7 @@ Example:
logrus.Fatal(err) logrus.Fatal(err)
} }
secrets, err := secret.ReadSecretsConfig(app.Path, composeFiles, app.Recipe) secrets, err := secret.ReadSecretsConfig(app.Path, composeFiles, app.StackName())
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }

2
go.mod
View File

@ -4,7 +4,7 @@ go 1.21
require ( require (
coopcloud.tech/tagcmp v0.0.0-20211103052201-885b22f77d52 coopcloud.tech/tagcmp v0.0.0-20211103052201-885b22f77d52
git.coopcloud.tech/coop-cloud/godotenv v1.5.2-0.20231130100106-7462d91acefd git.coopcloud.tech/coop-cloud/godotenv v1.5.2-0.20231130100509-01bff8284355
github.com/AlecAivazis/survey/v2 v2.3.7 github.com/AlecAivazis/survey/v2 v2.3.7
github.com/Gurpartap/logrus-stack v0.0.0-20170710170904-89c00d8a28f4 github.com/Gurpartap/logrus-stack v0.0.0-20170710170904-89c00d8a28f4
github.com/docker/cli v24.0.7+incompatible github.com/docker/cli v24.0.7+incompatible

4
go.sum
View File

@ -51,8 +51,8 @@ coopcloud.tech/tagcmp v0.0.0-20211103052201-885b22f77d52/go.mod h1:ESVm0wQKcbcFi
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
git.coopcloud.tech/coop-cloud/godotenv v1.5.2-0.20231130100106-7462d91acefd h1:dctCkMhcsgIWMrkB1Br8S0RJF17eG+LKiqcXXVr3mdU= git.coopcloud.tech/coop-cloud/godotenv v1.5.2-0.20231130100509-01bff8284355 h1:tCv2B4qoN6RMheKDnCzIafOkWS5BB1h7hwhmo+9bVeE=
git.coopcloud.tech/coop-cloud/godotenv v1.5.2-0.20231130100106-7462d91acefd/go.mod h1:Q8V1zbtPAlzYSr/Dvky3wS6x58IQAl3rot2me1oSO2Q= git.coopcloud.tech/coop-cloud/godotenv v1.5.2-0.20231130100509-01bff8284355/go.mod h1:Q8V1zbtPAlzYSr/Dvky3wS6x58IQAl3rot2me1oSO2Q=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230106234847-43070de90fa1 h1:EKPd1INOIyr5hWOWhvpmQpY6tKjeG0hT1s3AMC/9fic= github.com/AdaLogics/go-fuzz-headers v0.0.0-20230106234847-43070de90fa1 h1:EKPd1INOIyr5hWOWhvpmQpY6tKjeG0hT1s3AMC/9fic=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230106234847-43070de90fa1/go.mod h1:VzwV+t+dZ9j/H867F1M2ziD+yLHtB46oM35FxxMJ4d0= github.com/AdaLogics/go-fuzz-headers v0.0.0-20230106234847-43070de90fa1/go.mod h1:VzwV+t+dZ9j/H867F1M2ziD+yLHtB46oM35FxxMJ4d0=
github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ= github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ=

View File

@ -50,23 +50,30 @@ type App struct {
Path string Path string
} }
// StackName gets whatever the docker safe (uses the right delimiting // See documentation of config.StackName
// character, e.g. "_") stack name is for the app. In general, you don't want
// to use this to show anything to end-users, you want use a.Name instead.
func (a App) StackName() string { func (a App) StackName() string {
if _, exists := a.Env["STACK_NAME"]; exists { if _, exists := a.Env["STACK_NAME"]; exists {
return a.Env["STACK_NAME"] return a.Env["STACK_NAME"]
} }
stackName := SanitiseAppName(a.Name) stackName := StackName(a.Name)
a.Env["STACK_NAME"] = stackName
return stackName
}
// StackName gets whatever the docker safe (uses the right delimiting
// character, e.g. "_") stack name is for the app. In general, you don't want
// to use this to show anything to end-users, you want use a.Name instead.
func StackName(appName string) string {
stackName := SanitiseAppName(appName)
if len(stackName) > 45 { if len(stackName) > 45 {
logrus.Debugf("trimming %s to %s to avoid runtime limits", stackName, stackName[:45]) logrus.Debugf("trimming %s to %s to avoid runtime limits", stackName, stackName[:45])
stackName = stackName[:45] stackName = stackName[:45]
} }
a.Env["STACK_NAME"] = stackName
return stackName return stackName
} }

View File

@ -21,11 +21,24 @@ import (
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
// SecretValue represents a parsed `SECRET_FOO=v1 # length=bar` env var config // Secret represents a secret.
// secret definition. type Secret struct {
type SecretValue struct { // Version comes from the secret version environment variable.
// For example:
// SECRET_FOO=v1
Version string Version string
Length int // Length comes from the length modifier at the secret version environment
// variable. For Example:
// SECRET_FOO=v1 # length=12
Length int
// RemoteName is the name of the secret on the server. For example:
// name: ${STACK_NAME}_test_pass_two_${SECRET_TEST_PASS_TWO_VERSION}
// With the following:
// STACK_NAME=test_example_com
// SECRET_TEST_PASS_TWO_VERSION=v2
// Will have this remote name:
// test_example_com_test_pass_two_v2
RemoteName string
} }
// GeneratePasswords generates passwords. // GeneratePasswords generates passwords.
@ -67,11 +80,13 @@ func GeneratePassphrases(count uint) ([]string, error) {
// and some times you don't (as the caller). We need to be able to handle the // and some times you don't (as the caller). We need to be able to handle the
// "app new" case where we pass in the .env.sample and the "secret generate" // "app new" case where we pass in the .env.sample and the "secret generate"
// case where the app is created. // case where the app is created.
func ReadSecretsConfig(appEnvPath string, composeFiles []string, recipeName string) (map[string]SecretValue, error) { func ReadSecretsConfig(appEnvPath string, composeFiles []string, stackName string) (map[string]Secret, error) {
appEnv, appModifiers, err := config.ReadEnvWithModifiers(appEnvPath) appEnv, appModifiers, err := config.ReadEnvWithModifiers(appEnvPath)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Set the STACK_NAME to be able to generate the remote name correctly.
appEnv["STACK_NAME"] = stackName
opts := stack.Deploy{Composefiles: composeFiles} opts := stack.Deploy{Composefiles: composeFiles}
config, err := loader.LoadComposefile(opts, appEnv) config, err := loader.LoadComposefile(opts, appEnv)
@ -95,7 +110,7 @@ func ReadSecretsConfig(appEnvPath string, composeFiles []string, recipeName stri
return nil, nil return nil, nil
} }
secretValues := map[string]SecretValue{} secretValues := map[string]Secret{}
for secretId, secretConfig := range config.Secrets { for secretId, secretConfig := range config.Secrets {
if string(secretConfig.Name[len(secretConfig.Name)-1]) == "_" { if string(secretConfig.Name[len(secretConfig.Name)-1]) == "_" {
return nil, fmt.Errorf("missing version for secret? (%s)", secretId) return nil, fmt.Errorf("missing version for secret? (%s)", secretId)
@ -108,7 +123,7 @@ func ReadSecretsConfig(appEnvPath string, composeFiles []string, recipeName stri
lastIdx := strings.LastIndex(secretConfig.Name, "_") lastIdx := strings.LastIndex(secretConfig.Name, "_")
secretVersion := secretConfig.Name[lastIdx+1:] secretVersion := secretConfig.Name[lastIdx+1:]
value := SecretValue{Version: secretVersion} value := Secret{Version: secretVersion, RemoteName: secretConfig.Name}
// Check if the length modifier is set for this secret. // Check if the length modifier is set for this secret.
for k, v := range appModifiers { for k, v := range appModifiers {
@ -133,7 +148,7 @@ func ReadSecretsConfig(appEnvPath string, composeFiles []string, recipeName stri
} }
// GenerateSecrets generates secrets locally and sends them to a remote server for storage. // GenerateSecrets generates secrets locally and sends them to a remote server for storage.
func GenerateSecrets(cl *dockerClient.Client, secrets map[string]SecretValue, appName, server string) (map[string]string, error) { func GenerateSecrets(cl *dockerClient.Client, secrets map[string]Secret, server string) (map[string]string, error) {
secretsGenerated := map[string]string{} secretsGenerated := map[string]string{}
var mutex sync.Mutex var mutex sync.Mutex
var wg sync.WaitGroup var wg sync.WaitGroup
@ -141,11 +156,10 @@ func GenerateSecrets(cl *dockerClient.Client, secrets map[string]SecretValue, ap
for n, v := range secrets { for n, v := range secrets {
wg.Add(1) wg.Add(1)
go func(secretName string, secret SecretValue) { go func(secretName string, secret Secret) {
defer wg.Done() defer wg.Done()
secretRemoteName := fmt.Sprintf("%s_%s_%s", appName, secretName, secret.Version) logrus.Debugf("attempting to generate and store %s on %s", secret.RemoteName, server)
logrus.Debugf("attempting to generate and store %s on %s", secretRemoteName, server)
if secret.Length > 0 { if secret.Length > 0 {
passwords, err := GeneratePasswords(1, uint(secret.Length)) passwords, err := GeneratePasswords(1, uint(secret.Length))
@ -154,9 +168,9 @@ func GenerateSecrets(cl *dockerClient.Client, secrets map[string]SecretValue, ap
return return
} }
if err := client.StoreSecret(cl, secretRemoteName, passwords[0], server); err != nil { if err := client.StoreSecret(cl, secret.RemoteName, passwords[0], server); err != nil {
if strings.Contains(err.Error(), "AlreadyExists") { if strings.Contains(err.Error(), "AlreadyExists") {
logrus.Warnf("%s already exists, moving on...", secretRemoteName) logrus.Warnf("%s already exists, moving on...", secret.RemoteName)
ch <- nil ch <- nil
} else { } else {
ch <- err ch <- err
@ -174,9 +188,9 @@ func GenerateSecrets(cl *dockerClient.Client, secrets map[string]SecretValue, ap
return return
} }
if err := client.StoreSecret(cl, secretRemoteName, passphrases[0], server); err != nil { if err := client.StoreSecret(cl, secret.RemoteName, passphrases[0], server); err != nil {
if strings.Contains(err.Error(), "AlreadyExists") { if strings.Contains(err.Error(), "AlreadyExists") {
logrus.Warnf("%s already exists, moving on...", secretRemoteName) logrus.Warnf("%s already exists, moving on...", secret.RemoteName)
ch <- nil ch <- nil
} else { } else {
ch <- err ch <- err
@ -225,7 +239,7 @@ func PollSecretsStatus(cl *dockerClient.Client, app config.App) (secretStatuses,
return secStats, err return secStats, err
} }
secretsConfig, err := ReadSecretsConfig(app.Path, composeFiles, app.Recipe) secretsConfig, err := ReadSecretsConfig(app.Path, composeFiles, app.StackName())
if err != nil { if err != nil {
return secStats, err return secStats, err
} }

View File

@ -1,42 +1,30 @@
package secret package secret
import ( import (
"path"
"testing" "testing"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/upstream/stack"
loader "coopcloud.tech/abra/pkg/upstream/stack"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestReadSecretsConfig(t *testing.T) { func TestReadSecretsConfig(t *testing.T) {
offline := true composeFiles := []string{"./testdir/compose.yaml"}
recipe, err := recipe.Get("matrix-synapse", offline) secretsFromConfig, err := ReadSecretsConfig("./testdir/.env.sample", composeFiles, "test_example_com")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
sampleEnv, err := recipe.SampleEnv() // Simple secret
if err != nil { assert.Equal(t, "test_example_com_test_pass_one_v2", secretsFromConfig["test_pass_one"].RemoteName)
t.Fatal(err) assert.Equal(t, "v2", secretsFromConfig["test_pass_one"].Version)
} assert.Equal(t, 0, secretsFromConfig["test_pass_one"].Length)
composeFiles := []string{path.Join(config.RECIPES_DIR, recipe.Name, "compose.yml")} // Has a length modifier
envSamplePath := path.Join(config.RECIPES_DIR, recipe.Name, ".env.sample") assert.Equal(t, "test_example_com_test_pass_two_v1", secretsFromConfig["test_pass_two"].RemoteName)
secretsFromConfig, err := ReadSecretsConfig(envSamplePath, composeFiles, recipe.Name) assert.Equal(t, "v1", secretsFromConfig["test_pass_two"].Version)
if err != nil { assert.Equal(t, 10, secretsFromConfig["test_pass_two"].Length)
t.Fatal(err)
}
opts := stack.Deploy{Composefiles: composeFiles} // Secret name does not include the secret id
config, err := loader.LoadComposefile(opts, sampleEnv) assert.Equal(t, "test_example_com_pass_three_v2", secretsFromConfig["test_pass_three"].RemoteName)
if err != nil { assert.Equal(t, "v2", secretsFromConfig["test_pass_three"].Version)
t.Fatal(err) assert.Equal(t, 0, secretsFromConfig["test_pass_three"].Length)
}
for secretId := range config.Secrets {
assert.Contains(t, secretsFromConfig, secretId)
}
} }

View File

@ -0,0 +1,3 @@
SECRET_TEST_PASS_ONE_VERSION=v2
SECRET_TEST_PASS_TWO_VERSION=v1 # length=10
SECRET_TEST_PASS_THREE_VERSION=v2

View File

@ -0,0 +1,21 @@
---
version: "3.8"
services:
app:
image: nginx:1.21.0
secrets:
- test_pass_one
- test_pass_two
- test_pass_three
secrets:
test_pass_one:
external: true
name: ${STACK_NAME}_test_pass_one_${SECRET_TEST_PASS_ONE_VERSION} # should be removed
test_pass_two:
external: true
name: ${STACK_NAME}_test_pass_two_${SECRET_TEST_PASS_TWO_VERSION}
test_pass_three:
external: true
name: ${STACK_NAME}_pass_three_${SECRET_TEST_PASS_THREE_VERSION} # secretId and name don't match

View File

@ -18,7 +18,7 @@ import (
// //
// ssh://<user>@<host> URL requires Docker 18.09 or later on the remote host. // ssh://<user>@<host> URL requires Docker 18.09 or later on the remote host.
func GetConnectionHelper(daemonURL string) (*connhelper.ConnectionHelper, error) { func GetConnectionHelper(daemonURL string) (*connhelper.ConnectionHelper, error) {
return getConnectionHelper(daemonURL, []string{"-o ConnectTimeout=5"}) return getConnectionHelper(daemonURL, []string{"-o ConnectTimeout=60"})
} }
func getConnectionHelper(daemonURL string, sshFlags []string) (*connhelper.ConnectionHelper, error) { func getConnectionHelper(daemonURL string, sshFlags []string) (*connhelper.ConnectionHelper, error) {