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")
secretsConfig, err := secret.ReadSecretsConfig(envSamplePath, composeFiles, recipe.Name)
secretsConfig, err := secret.ReadSecretsConfig(envSamplePath, composeFiles, config.StackName(internal.Domain))
if err != nil {
return err
}
@ -168,14 +168,8 @@ var appNewCommand = cli.Command{
type AppSecrets map[string]string
// createSecrets creates all secrets for a new app.
func createSecrets(cl *dockerClient.Client, secretsConfig map[string]secret.SecretValue, sanitisedAppName string) (AppSecrets, error) {
// NOTE(d1): trim to match app.StackName() implementation
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)
func createSecrets(cl *dockerClient.Client, secretsConfig map[string]secret.Secret, sanitisedAppName string) (AppSecrets, error) {
secrets, err := secret.GenerateSecrets(cl, secretsConfig, internal.NewAppServer)
if err != nil {
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.
func promptForSecrets(recipeName string, secretsConfig map[string]secret.SecretValue) error {
func promptForSecrets(recipeName string, secretsConfig map[string]secret.Secret) error {
if len(secretsConfig) == 0 {
logrus.Debugf("%s has no secrets to generate, skipping...", recipeName)
return nil

View File

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

2
go.mod
View File

@ -4,7 +4,7 @@ go 1.21
require (
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/Gurpartap/logrus-stack v0.0.0-20170710170904-89c00d8a28f4
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/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
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.20231130100106-7462d91acefd/go.mod h1:Q8V1zbtPAlzYSr/Dvky3wS6x58IQAl3rot2me1oSO2Q=
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.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/go.mod h1:VzwV+t+dZ9j/H867F1M2ziD+yLHtB46oM35FxxMJ4d0=
github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ=

View File

@ -50,23 +50,30 @@ type App struct {
Path string
}
// 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.
// See documentation of config.StackName
func (a App) StackName() string {
if _, exists := a.Env["STACK_NAME"]; exists {
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 {
logrus.Debugf("trimming %s to %s to avoid runtime limits", stackName, stackName[:45])
stackName = stackName[:45]
}
a.Env["STACK_NAME"] = stackName
return stackName
}

View File

@ -21,11 +21,24 @@ import (
"github.com/sirupsen/logrus"
)
// SecretValue represents a parsed `SECRET_FOO=v1 # length=bar` env var config
// secret definition.
type SecretValue struct {
// Secret represents a secret.
type Secret struct {
// Version comes from the secret version environment variable.
// For example:
// SECRET_FOO=v1
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.
@ -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
// "app new" case where we pass in the .env.sample and the "secret generate"
// 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)
if err != nil {
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}
config, err := loader.LoadComposefile(opts, appEnv)
@ -95,7 +110,7 @@ func ReadSecretsConfig(appEnvPath string, composeFiles []string, recipeName stri
return nil, nil
}
secretValues := map[string]SecretValue{}
secretValues := map[string]Secret{}
for secretId, secretConfig := range config.Secrets {
if string(secretConfig.Name[len(secretConfig.Name)-1]) == "_" {
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, "_")
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.
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.
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{}
var mutex sync.Mutex
var wg sync.WaitGroup
@ -141,11 +156,10 @@ func GenerateSecrets(cl *dockerClient.Client, secrets map[string]SecretValue, ap
for n, v := range secrets {
wg.Add(1)
go func(secretName string, secret SecretValue) {
go func(secretName string, secret Secret) {
defer wg.Done()
secretRemoteName := fmt.Sprintf("%s_%s_%s", appName, secretName, secret.Version)
logrus.Debugf("attempting to generate and store %s on %s", secretRemoteName, server)
logrus.Debugf("attempting to generate and store %s on %s", secret.RemoteName, server)
if secret.Length > 0 {
passwords, err := GeneratePasswords(1, uint(secret.Length))
@ -154,9 +168,9 @@ func GenerateSecrets(cl *dockerClient.Client, secrets map[string]SecretValue, ap
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") {
logrus.Warnf("%s already exists, moving on...", secretRemoteName)
logrus.Warnf("%s already exists, moving on...", secret.RemoteName)
ch <- nil
} else {
ch <- err
@ -174,9 +188,9 @@ func GenerateSecrets(cl *dockerClient.Client, secrets map[string]SecretValue, ap
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") {
logrus.Warnf("%s already exists, moving on...", secretRemoteName)
logrus.Warnf("%s already exists, moving on...", secret.RemoteName)
ch <- nil
} else {
ch <- err
@ -225,7 +239,7 @@ func PollSecretsStatus(cl *dockerClient.Client, app config.App) (secretStatuses,
return secStats, err
}
secretsConfig, err := ReadSecretsConfig(app.Path, composeFiles, app.Recipe)
secretsConfig, err := ReadSecretsConfig(app.Path, composeFiles, app.StackName())
if err != nil {
return secStats, err
}

View File

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

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.
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) {