forked from toolshed/abra
		
	Compare commits
	
		
			1 Commits
		
	
	
		
			test-refac
			...
			fix-secret
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 964d4efca4 | 
| @ -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 | ||||||
|  | |||||||
| @ -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) | ||||||
| 		} | 		} | ||||||
|  | |||||||
| @ -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 | ||||||
| } | } | ||||||
|  |  | ||||||
|  | |||||||
| @ -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 comes from the length modifier at the secret version environment | ||||||
|  | 	// variable. For Example: | ||||||
|  | 	//   SECRET_FOO=v1 # length=12 | ||||||
| 	Length int | 	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 | ||||||
| 	} | 	} | ||||||
|  | |||||||
| @ -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) |  | ||||||
| 	} |  | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										3
									
								
								pkg/secret/testdir/.env.sample
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								pkg/secret/testdir/.env.sample
									
									
									
									
									
										Normal 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 | ||||||
							
								
								
									
										21
									
								
								pkg/secret/testdir/compose.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								pkg/secret/testdir/compose.yaml
									
									
									
									
									
										Normal 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 | ||||||
		Reference in New Issue
	
	Block a user
	