From affd96e2ef31b4943455e207ab34863df0c8e8b9 Mon Sep 17 00:00:00 2001
From: lijianguo <lijianguo1991@outlook.com>
Date: Fri, 15 Nov 2024 17:21:54 +0800
Subject: [PATCH] feat(workflow-cli): config add ValuesRefs field

---
 cmd/cmd.go               |  21 +--
 cmd/config.go            | 323 ++++++++++++++++++++++++---------------
 cmd/config_test.go       | 276 ++++++++++-----------------------
 cmd/healthchecks_test.go |  10 +-
 cmd/limits_test.go       |  14 +-
 cmd/pts.go               |  27 +++-
 cmd/pts_test.go          |  37 ++++-
 cmd/registry.go          |  93 +++++------
 cmd/registry_test.go     | 108 +++----------
 cmd/resources.go         |  12 +-
 cmd/resources_test.go    |   2 +-
 cmd/tags_test.go         |   6 +-
 cmd/timeouts_test.go     |  14 +-
 cmd/utils.go             |  21 +++
 cmd/volumes_test.go      |   2 +-
 go.mod                   |   2 +-
 go.sum                   |   4 +-
 parser/auth.go           |   4 +-
 parser/builds.go         |   2 +-
 parser/config.go         | 125 ++++++++++++---
 parser/config_test.go    |  32 +++-
 parser/domains.go        |   4 +-
 parser/healthchecks.go   |  12 +-
 parser/limits.go         |   6 +-
 parser/pts.go            |  28 ++++
 parser/pts_test.go       |   8 +
 parser/registry.go       |  39 ++---
 parser/registry_test.go  |  10 +-
 parser/releases.go       |   2 +-
 parser/resources.go      |   7 +-
 parser/resources_test.go |   2 +-
 parser/tags.go           |   2 +-
 parser/timeouts.go       |   2 +-
 parser/tokens.go         |   4 +-
 34 files changed, 685 insertions(+), 576 deletions(-)

diff --git a/cmd/cmd.go b/cmd/cmd.go
index b5599f9b..6f8aa834 100644
--- a/cmd/cmd.go
+++ b/cmd/cmd.go
@@ -42,11 +42,13 @@ type Commander interface {
 	CertInfo(string, string) error
 	CertAttach(string, string, string) error
 	CertDetach(string, string, string) error
-	ConfigList(string, string, int) error
-	ConfigSet(string, string, []string, string) error
-	ConfigUnset(string, string, []string, string) error
-	ConfigPull(string, string, string, bool, bool) error
-	ConfigPush(string, string, string, string) error
+	ConfigInfo(string, string, string, int) error
+	ConfigSet(string, string, string, []string, string) error
+	ConfigUnset(string, string, string, []string, string) error
+	ConfigPull(string, string, string, string, bool, bool) error
+	ConfigPush(string, string, string, string, string) error
+	ConfigAttach(string, string, string) error
+	ConfigDetach(string, string, string) error
 	DomainsList(string, int) error
 	DomainsAdd(string, string, string) error
 	DomainsRemove(string, string) error
@@ -95,9 +97,10 @@ type Commander interface {
 	PtsDescribe(string, string) error
 	PtsScale(string, []string) error
 	PtsRestart(string, []string, string) error
-	RegistryList(string, int) error
-	RegistrySet(string, []string) error
-	RegistryUnset(string, []string) error
+	PtsClean(string, []string) error
+	RegistryList(string, string, int) error
+	RegistrySet(string, string, string, string) error
+	RegistryUnset(string, string) error
 	ReleasesList(string, string, int) error
 	ReleasesInfo(string, int) error
 	ReleasesDeploy(string, []string, bool, string) error
@@ -139,7 +142,7 @@ type Commander interface {
 	ResourcesCreate(string, string, string, []string, string) error
 	ResourcesList(string, int) error
 	ResourceDelete(string, string, string) error
-	ResourceGet(string, string) error
+	ResourceGet(string, string, bool) error
 	ResourcePut(string, string, string, []string, string) error
 	ResourceBind(string, string) error
 	ResourceUnbind(string, string) error
diff --git a/cmd/config.go b/cmd/config.go
index 6a5464cc..3b0aa852 100644
--- a/cmd/config.go
+++ b/cmd/config.go
@@ -3,7 +3,6 @@ package cmd
 import (
 	"bufio"
 	"bytes"
-	"encoding/base64"
 	"fmt"
 	"os"
 	"regexp"
@@ -14,103 +13,103 @@ import (
 	"github.com/drycc/controller-sdk-go/api"
 	"github.com/drycc/controller-sdk-go/appsettings"
 	"github.com/drycc/controller-sdk-go/config"
+	"sigs.k8s.io/yaml"
 )
 
-// ConfigList lists an app's config.
-func (d *DryccCmd) ConfigList(appID string, ptype string, version int) error {
+// ConfigInfo for an app
+func (d *DryccCmd) ConfigInfo(appID string, ptype string, group string, version int) error {
 	s, appID, err := load(d.ConfigFile, appID)
 	if err != nil {
 		return err
 	}
+
 	config, err := config.List(s.Client, appID, version)
 	if d.checkAPICompatibility(s.Client, err) != nil {
 		return err
 	}
-
-	table := d.getDefaultFormatTable([]string{"PTYPE", "NAME", "VALUE"})
-	for _, key := range *sortKeys(config.Values) {
-		table.Append([]string{"N/A", key, fmt.Sprintf("%v", config.Values[key])})
-	}
-	for key, values := range config.TypedValues {
-		if ptype == "" || ptype == key {
-			for _, key2 := range *sortKeys(values) {
-				table.Append([]string{key, key2, fmt.Sprintf("%v", values[key2])})
+	// init output struct
+	cv := api.ConfigInfo{
+		Group: make(map[string][]api.KV),
+		Ptype: make(map[string]api.PtypeValue),
+	}
+	for _, value := range sortConfigValues(config.Values) {
+		// display the selected or all
+		if (ptype != "" && value.Ptype == ptype) ||
+			(group != "" && value.Group == group) ||
+			(ptype == "" && group == "") {
+			if value.Group != "" {
+				cv.Group[value.Group] = append(cv.Group[value.Group], api.KV{Name: value.Name, Value: value.Value})
+			} else if value.Ptype != "" {
+				temp := cv.Ptype[value.Ptype]
+				temp.Env = append(temp.Env, api.KV{Name: value.Name, Value: value.Value})
+				cv.Ptype[value.Ptype] = temp
+
+				if len(config.ValuesRefs[value.Ptype]) != 0 {
+					temp.Ref = config.ValuesRefs[value.Ptype]
+					cv.Ptype[value.Ptype] = temp
+				}
 			}
 		}
 	}
-	table.Render()
+	if len(cv.Ptype) == 0 && len(cv.Group) == 0 {
+		d.Println()
+		return nil
+	}
+
+	c, err := yaml.Marshal(cv)
+	if err != nil {
+		return err
+	}
+	d.Println(string(c))
 	return nil
 }
 
 // ConfigSet sets an app's config variables.
-func (d *DryccCmd) ConfigSet(appID string, ptype string, configVars []string, confirm string) error {
+func (d *DryccCmd) ConfigSet(appID string, ptype string, group string, configVars []string, confirm string) error {
 	s, appID, err := load(d.ConfigFile, appID)
 
 	if err != nil {
 		return err
 	}
-
-	err = configConfirmAction(s.Client, appID, ptype, confirm)
+	if ptype == "" && group == "" {
+		group = "global"
+	}
+	err = configConfirmAction(s.Client, appID, ptype, group, confirm)
 	if err != nil {
 		return err
 	}
 
-	configMap, err := parseConfig(configVars)
+	configMap, err := parseConfig(ptype, group, configVars)
 	if err != nil {
 		return err
 	}
 
-	if value, ok := configMap["SSH_KEY"]; ok {
-		sshKey, err := parseSSHKey(value.(string))
-		if err != nil {
-			return err
-		}
-		configMap["SSH_KEY"] = base64.StdEncoding.EncodeToString([]byte(sshKey))
-	}
-
-	// NOTE(bacongobbler): check if the user is using the old way to set healthchecks. If so,
-	// send them a deprecation notice.
-	for key := range configMap {
-		if strings.Contains(key, "HEALTHCHECK_") {
-			d.Println(`Hey there! We've noticed that you're using 'drycc config:set HEALTHCHECK_URL'
-to set up healthchecks. This functionality has been deprecated. In the future, please use
-'drycc healthchecks' to set up application health checks. Thanks!`)
-		}
-	}
-
 	d.Print("Creating config... ")
 
 	quit := progress(d.WOut)
-	configObj, err := config.Set(s.Client, appID, func() api.Config {
-		if ptype != "" {
-			return api.Config{TypedValues: map[string]api.ConfigValues{ptype: configMap}}
-		}
-		return api.Config{Values: configMap}
-	}())
+	configObj := api.Config{Values: configMap}
+	_, err = config.Set(s.Client, appID, configObj)
 	quit <- true
 	<-quit
 	if d.checkAPICompatibility(s.Client, err) != nil {
 		return err
 	}
-
-	if release, ok := configObj.Values["WORKFLOW_RELEASE"]; ok {
-		d.Printf("done, %s\n\n", release)
-	} else {
-		d.Print("done\n\n")
-	}
-
-	return d.ConfigList(appID, ptype, -1)
+	d.Print("done\n\n")
+	return nil
 }
 
 // ConfigUnset removes a config variable from an app.
-func (d *DryccCmd) ConfigUnset(appID string, ptype string, configVars []string, confirm string) error {
+func (d *DryccCmd) ConfigUnset(appID string, ptype string, group string, configVars []string, confirm string) error {
 	s, appID, err := load(d.ConfigFile, appID)
 
 	if err != nil {
 		return err
 	}
 
-	err = configConfirmAction(s.Client, appID, ptype, confirm)
+	if ptype == "" && group == "" {
+		group = "global"
+	}
+	err = configConfirmAction(s.Client, appID, ptype, group, confirm)
 	if err != nil {
 		return err
 	}
@@ -118,23 +117,21 @@ func (d *DryccCmd) ConfigUnset(appID string, ptype string, configVars []string,
 	d.Print("Removing config... ")
 
 	quit := progress(d.WOut)
-
-	configObj := api.Config{}
-
-	valuesMap := make(map[string]interface{})
-
+	valuesMaps := []api.ConfigValue{}
 	for _, configVar := range configVars {
-		valuesMap[configVar] = nil
+		valuesMap := api.ConfigValue{
+			Ptype: ptype,
+			Group: group,
+			KV: api.KV{
+				Name:  configVar,
+				Value: "",
+			},
+		}
+		valuesMaps = append(valuesMaps, valuesMap)
 	}
 
-	configObj.Values = valuesMap
-
-	configObj, err = config.Set(s.Client, appID, func() api.Config {
-		if ptype != "" {
-			return api.Config{TypedValues: map[string]api.ConfigValues{ptype: valuesMap}}
-		}
-		return api.Config{Values: valuesMap}
-	}())
+	configObj := api.Config{Values: valuesMaps}
+	_, err = config.Set(s.Client, appID, configObj)
 
 	quit <- true
 	<-quit
@@ -142,23 +139,25 @@ func (d *DryccCmd) ConfigUnset(appID string, ptype string, configVars []string,
 		return err
 	}
 
-	if release, ok := configObj.Values["WORKFLOW_RELEASE"]; ok {
-		d.Printf("done, %s\n\n", release)
-	} else {
-		d.Print("done\n\n")
-	}
-
-	return d.ConfigList(appID, ptype, -1)
+	d.Print("done\n\n")
+	return nil
 }
 
 // ConfigPull pulls an app's config to a file.
-func (d *DryccCmd) ConfigPull(appID, ptype, fileName string, interactive bool, overwrite bool) error {
+func (d *DryccCmd) ConfigPull(appID, ptype, group, fileName string, interactive bool, overwrite bool) error {
 	s, appID, err := load(d.ConfigFile, appID)
 
 	if err != nil {
 		return err
 	}
 
+	if ptype == "" && group == "" {
+		group = "global"
+	}
+	if ptype != "" && group != "" {
+		d.Println("Only one of ptype and group can be selected.")
+		return nil
+	}
 	configVars, err := config.List(s.Client, appID, -1)
 	if d.checkAPICompatibility(s.Client, err) != nil {
 		return err
@@ -169,9 +168,13 @@ func (d *DryccCmd) ConfigPull(appID, ptype, fileName string, interactive bool, o
 	if err != nil {
 		return err
 	}
-	configValues := configVars.Values
-	if ptype != "" {
-		configValues = configVars.TypedValues[ptype]
+	configValues := []api.ConfigValue{}
+
+	for _, value := range configVars.Values {
+		if (ptype != "" && value.Ptype == ptype) ||
+			(group != "" && value.Group == group) {
+			configValues = append(configValues, value)
+		}
 	}
 
 	if (stat.Mode() & os.ModeCharDevice) == 0 {
@@ -191,34 +194,34 @@ func (d *DryccCmd) ConfigPull(appID, ptype, fileName string, interactive bool, o
 			return err
 		}
 		localConfigVars := strings.Split(string(contents), "\n")
-		configMap, err := parseConfig(localConfigVars[:len(localConfigVars)-1])
+		configMap, err := parseEnv(localConfigVars[:len(localConfigVars)-1])
 		if err != nil {
 			return err
 		}
-		for key, value := range configValues {
-			localValue, ok := configMap[key]
+		for _, value := range configValues {
+			localValue, ok := configMap[value.Name]
 			if ok {
-				if value != localValue {
+				if value.Value != localValue {
 					var confirm string
-					d.Printf("%s: overwrite %s with %s? (y/N) ", key, localValue, value)
+					d.Printf("%s: overwrite %s with %s? (y/N) ", value.Name, localValue, value)
 
 					fmt.Scanln(&confirm)
 
 					if strings.ToLower(confirm) == "y" {
-						configMap[key] = value
+						configMap[value.Name] = value.Value
 					}
 				}
 			} else {
-				configMap[key] = value
+				configMap[value.Name] = value.Value
 			}
 		}
-		return os.WriteFile(fileName, []byte(formatConfig(configMap)), 0755)
+		return os.WriteFile(fileName, []byte(formatEnv(configMap)), 0755)
 	}
 	return os.WriteFile(fileName, []byte(formatConfig(configValues)), 0755)
 }
 
 // ConfigPush pushes an app's config from a file.
-func (d *DryccCmd) ConfigPush(appID, ptype string, fileName string, confirm string) error {
+func (d *DryccCmd) ConfigPush(appID, ptype string, group string, fileName string, confirm string) error {
 	stat, err := os.Stdin.Stat()
 
 	if err != nil {
@@ -228,11 +231,14 @@ func (d *DryccCmd) ConfigPush(appID, ptype string, fileName string, confirm stri
 	if err != nil {
 		return err
 	}
+	if ptype == "" && group == "" {
+		group = "global"
+	}
 	var contents []byte
 
 	if (stat.Mode() & os.ModeCharDevice) == 0 {
 
-		err = configConfirmActionStdin(s.Client, appID, ptype, confirm)
+		err = configConfirmActionStdin(s.Client, appID, ptype, group, confirm)
 		if err != nil {
 			return err
 		}
@@ -240,7 +246,7 @@ func (d *DryccCmd) ConfigPush(appID, ptype string, fileName string, confirm stri
 		buffer.ReadFrom(os.Stdin)
 		contents = buffer.Bytes()
 	} else {
-		err = configConfirmAction(s.Client, appID, ptype, confirm)
+		err = configConfirmAction(s.Client, appID, ptype, group, confirm)
 		if err != nil {
 			return err
 		}
@@ -263,11 +269,65 @@ func (d *DryccCmd) ConfigPush(appID, ptype string, fileName string, confirm stri
 		}
 	}
 
-	return d.ConfigSet(appID, ptype, config, "yes")
+	return d.ConfigSet(appID, ptype, group, config, "yes")
+}
+
+func (d *DryccCmd) ConfigAttach(appID string, ptype string, groups string) error {
+	s, appID, err := load(d.ConfigFile, appID)
+
+	if err != nil {
+		return err
+	}
+
+	d.Print("Attach config... ")
+
+	quit := progress(d.WOut)
+	gs := strings.Split(groups, ",")
+	refs := api.ValuesRefs{
+		ptype: gs,
+	}
+	configObj := api.Config{ValuesRefs: refs}
+	_, err = config.Set(s.Client, appID, configObj)
+
+	quit <- true
+	<-quit
+	if d.checkAPICompatibility(s.Client, err) != nil {
+		return err
+	}
+
+	d.Print("done\n\n")
+	return nil
+}
+
+func (d *DryccCmd) ConfigDetach(appID string, ptype string, groups string) error {
+	s, appID, err := load(d.ConfigFile, appID)
+
+	if err != nil {
+		return err
+	}
+
+	d.Print("Detach config... ")
+
+	quit := progress(d.WOut)
+	gs := strings.Split(groups, ",")
+	refs := api.ValuesRefs{
+		ptype: gs,
+	}
+	configObj := api.Config{ValuesRefs: refs}
+	err = config.Detach(s.Client, appID, configObj)
+
+	quit <- true
+	<-quit
+	if d.checkAPICompatibility(s.Client, err) != nil {
+		return err
+	}
+
+	d.Print("done\n\n")
+	return nil
 }
 
-func parseConfig(configVars []string) (api.ConfigValues, error) {
-	configMap := make(api.ConfigValues)
+func parseConfig(ptype, group string, configVars []string) ([]api.ConfigValue, error) {
+	configMap := []api.ConfigValue{}
 
 	regex := regexp.MustCompile(`^([A-z0-9_\-\.]+)=([\s\S]*)$`)
 	for _, config := range configVars {
@@ -278,7 +338,16 @@ func parseConfig(configVars []string) (api.ConfigValues, error) {
 
 		if regex.MatchString(config) {
 			captures := regex.FindStringSubmatch(config)
-			configMap[captures[1]] = captures[2]
+			value := api.ConfigValue{
+				Ptype: ptype,
+				Group: group,
+				KV: api.KV{
+					Name:  captures[1],
+					Value: captures[2],
+				},
+			}
+			configMap = append(configMap, value)
+
 		} else {
 			return nil, fmt.Errorf("'%s' does not match the pattern 'key=var', ex: MODE=test", config)
 		}
@@ -287,38 +356,28 @@ func parseConfig(configVars []string) (api.ConfigValues, error) {
 	return configMap, nil
 }
 
-func parseSSHKey(value string) (string, error) {
-	sshRegex := regexp.MustCompile("^-----BEGIN (DSA|RSA|EC|OPENSSH) PRIVATE KEY-----")
-
-	if sshRegex.MatchString(value) {
-		return value, nil
-	}
-
-	// NOTE(felixbuenemann): check if the current value is already a base64 encoded key.
-	// This is the case if it was fetched using "drycc config:pull".
-	contents, err := base64.StdEncoding.DecodeString(value)
-
-	if err == nil && sshRegex.MatchString(string(contents)) {
-		return string(contents), nil
-	}
+func parseEnv(configVars []string) (map[string]interface{}, error) {
+	configMap := make(map[string]interface{})
 
-	// NOTE(felixbuenemann): check if the value is a path to a private key.
-	if _, err := os.Stat(value); err == nil {
-		contents, err := os.ReadFile(value)
-
-		if err != nil {
-			return "", err
+	regex := regexp.MustCompile(`^([A-z0-9_\-\.]+)=([\s\S]*)$`)
+	for _, config := range configVars {
+		// Skip config that starts with an comment
+		if config[0] == '#' {
+			continue
 		}
 
-		if sshRegex.MatchString(string(contents)) {
-			return string(contents), nil
+		if regex.MatchString(config) {
+			captures := regex.FindStringSubmatch(config)
+			configMap[captures[1]] = captures[2]
+		} else {
+			return nil, fmt.Errorf("'%s' does not match the pattern 'key=var', ex: MODE=test", config)
 		}
 	}
 
-	return "", fmt.Errorf("could not parse SSH private key:\n %s", value)
+	return configMap, nil
 }
 
-func formatConfig(configVars map[string]interface{}) string {
+func formatEnv(configVars map[string]interface{}) string {
 	var formattedConfig string
 
 	keys := *sortKeys(configVars)
@@ -329,13 +388,32 @@ func formatConfig(configVars map[string]interface{}) string {
 	return formattedConfig
 }
 
-func configConfirmAction(s *drycc.Client, appID string, ptype string, confirm string) error {
+func formatConfig(configVars []api.ConfigValue) string {
+	var formattedConfig string
+
+	values := sortConfigValues(configVars)
+	for _, value := range values {
+		formattedConfig += fmt.Sprintf("%s=%v\n", value.Name, value.Value)
+	}
+
+	return formattedConfig
+}
+
+func configConfirmAction(s *drycc.Client, appID string, ptype string, group string, confirm string) error {
+
+	if ptype != "" && group != "" {
+		fmt.Println("Only one of ptype and group can be selected.")
+		return nil
+	} else if ptype == "" && group == "" {
+		group = "global"
+	}
+
 	appSettings, _ := appsettings.List(s, appID)
 	autodeploy := true
 	if appSettings.Autodeploy != nil && !*appSettings.Autodeploy {
 		autodeploy = false
 	}
-	if ptype == "" && (confirm == "" || confirm != "yes") && autodeploy {
+	if ptype == "" && group == "" && (confirm == "" || confirm != "yes") && autodeploy {
 		fmt.Printf(` !    WARNING: Potentially Config Action
  !    This command will deploy all processes of the application
  !    To proceed, type "yes" !
@@ -350,7 +428,7 @@ func configConfirmAction(s *drycc.Client, appID string, ptype string, confirm st
 	return nil
 }
 
-func configConfirmActionStdin(s *drycc.Client, appID string, ptype string, confirm string) error {
+func configConfirmActionStdin(s *drycc.Client, appID string, ptype string, group string, confirm string) error {
 	var reader *bufio.Reader
 	if runtime.GOOS == "windows" {
 		reader = bufio.NewReader(os.Stdin)
@@ -362,12 +440,19 @@ func configConfirmActionStdin(s *drycc.Client, appID string, ptype string, confi
 		defer file.Close()
 		reader = bufio.NewReader(file)
 	}
+
+	if ptype != "" && group != "" {
+		fmt.Println("Only one of ptype and group can be selected.")
+		return nil
+	}
+
 	appSettings, _ := appsettings.List(s, appID)
 	autodeploy := true
 	if appSettings.Autodeploy != nil && !*appSettings.Autodeploy {
 		autodeploy = false
 	}
-	if ptype == "" && (confirm == "" || confirm != "yes") && autodeploy {
+
+	if ptype == "" && group == "" && (confirm == "" || confirm != "yes") && autodeploy {
 		fmt.Printf(` !    WARNING: Potentially Config Action
  !    This command will deploy all processes of the application
  !    To proceed, type "yes" !
diff --git a/cmd/config_test.go b/cmd/config_test.go
index 49576287..afb3f62e 100644
--- a/cmd/config_test.go
+++ b/cmd/config_test.go
@@ -4,7 +4,6 @@ import (
 	"bytes"
 	"fmt"
 	"net/http"
-	"os"
 	"testing"
 
 	"github.com/drycc/controller-sdk-go/api"
@@ -15,56 +14,51 @@ import (
 func TestParseConfig(t *testing.T) {
 	t.Parallel()
 
-	_, err := parseConfig([]string{"FOO=bar", "CAR star"})
+	_, err := parseConfig("", "", []string{"FOO=bar", "CAR star"})
 	assert.NotEqual(t, err, nil, "config")
 
-	actual, err := parseConfig([]string{"FOO=bar"})
+	actual, err := parseConfig("", "", []string{"FOO=bar"})
 	assert.NoError(t, err)
-	assert.Equal(t, actual, api.ConfigValues{"FOO": "bar"}, "map")
+	assert.Equal(t, actual, []api.ConfigValue{{KV: api.KV{Name: "FOO", Value: "bar"}}}, "map")
 
-	actual, err = parseConfig([]string{"FOO="})
+	actual, err = parseConfig("", "", []string{"FOO="})
 	assert.NoError(t, err)
-	assert.Equal(t, actual, api.ConfigValues{"FOO": ""}, "map")
-}
-
-func TestParseSSHKey(t *testing.T) {
-	t.Parallel()
-
-	_, err := parseSSHKey("foobar")
-	assert.NotEqual(t, err, "bogus key")
-
-	validSSHKey := "-----BEGIN OPENSSH PRIVATE KEY-----"
-
-	actual, err := parseSSHKey(validSSHKey)
-	assert.NoError(t, err)
-	assert.Equal(t, actual, validSSHKey, "plain key")
-
-	encodedSSHKey := "LS0tLS1CRUdJTiBPUEVOU1NIIFBSSVZBVEUgS0VZLS0tLS0="
-
-	actual, err = parseSSHKey(encodedSSHKey)
-	assert.NoError(t, err)
-	assert.Equal(t, actual, validSSHKey, "base64 key")
-
-	keyFile, err := os.CreateTemp("", "drycc-cli-unit-test-sshkey")
-	assert.NoError(t, err)
-	defer os.Remove(keyFile.Name())
-	_, err = keyFile.Write([]byte(validSSHKey))
-	assert.NoError(t, err)
-	keyFile.Close()
-
-	actual, err = parseSSHKey(keyFile.Name())
-	assert.NoError(t, err)
-	assert.Equal(t, actual, validSSHKey, "key path")
+	assert.Equal(t, actual, []api.ConfigValue{{KV: api.KV{Name: "FOO", Value: ""}}}, "map")
 }
 
 func TestFormatConfig(t *testing.T) {
 	t.Parallel()
 
-	testMap := map[string]interface{}{
-		"TEST":  "testing",
-		"NCC":   1701,
-		"TRUE":  false,
-		"FLOAT": 12.34,
+	testMap := []api.ConfigValue{
+		{
+			Ptype: "web",
+			Group: "",
+			KV: api.KV{
+				Name:  "TEST",
+				Value: "testing",
+			},
+		}, {
+			Ptype: "web",
+			Group: "",
+			KV: api.KV{
+				Name:  "NCC",
+				Value: "1701",
+			},
+		}, {
+			Ptype: "web",
+			Group: "",
+			KV: api.KV{
+				Name:  "TRUE",
+				Value: false,
+			},
+		}, {
+			Ptype: "web",
+			Group: "",
+			KV: api.KV{
+				Name:  "FLOAT",
+				Value: 12.34,
+			},
+		},
 	}
 
 	testOut := formatConfig(testMap)
@@ -86,67 +80,6 @@ func TestSortKeys(t *testing.T) {
 	assert.Equal(t, *sortKeys(test), []string{"a", "b", "c", "d"}, "map")
 }
 
-func TestConfigList(t *testing.T) {
-	t.Parallel()
-	cf, server, err := testutil.NewTestServerAndClient()
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer server.Close()
-
-	server.Mux.HandleFunc("/v2/apps/foo/config/", func(w http.ResponseWriter, _ *http.Request) {
-		testutil.SetHeaders(w)
-		fmt.Fprintf(w, `{
-    "owner": "jkirk",
-    "app": "foo",
-    "values": {
-        "TEST":  "testing",
-        "NCC":   "1701",
-        "TRUE":  "false",
-        "FLOAT": "12.34"
-    },
-    "typed_values": {
-		"web": {
-            "PORT":  "9000"
-		}
-    },
-    "memory": {},
-    "cpu": {},
-    "tags": {},
-    "registry": {},
-    "created": "2014-01-01T00:00:00UTC",
-    "updated": "2014-01-01T00:00:00UTC",
-    "uuid": "de1bf5b5-4a72-4f94-a10c-d2a3741cdf75"
-}`)
-	})
-
-	var b bytes.Buffer
-	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
-
-	err = cmdr.ConfigList("foo", "", -1)
-	assert.NoError(t, err)
-
-	assert.Equal(t, b.String(), `PTYPE    NAME     VALUE   
-N/A      FLOAT    12.34      
-N/A      NCC      1701       
-N/A      TEST     testing    
-N/A      TRUE     false      
-web      PORT     9000       
-`, "output")
-
-	b.Reset()
-	err = cmdr.ConfigList("foo", "web", -1)
-	assert.NoError(t, err)
-	assert.Equal(t, b.String(), `PTYPE    NAME     VALUE   
-N/A      FLOAT    12.34      
-N/A      NCC      1701       
-N/A      TEST     testing    
-N/A      TRUE     false      
-web      PORT     9000       
-`, "output")
-
-}
-
 func TestConfigSet(t *testing.T) {
 	t.Parallel()
 	cf, server, err := testutil.NewTestServerAndClient()
@@ -159,9 +92,21 @@ func TestConfigSet(t *testing.T) {
 		testutil.SetHeaders(w)
 		if r.Method == "POST" {
 			testutil.AssertBody(t, api.Config{
-				Values: map[string]interface{}{
-					"TRUE":    "false",
-					"SSH_KEY": "LS0tLS1CRUdJTiBPUEVOU1NIIFBSSVZBVEUgS0VZLS0tLS0=",
+				Values: []api.ConfigValue{
+					{
+						Ptype: "web",
+						KV: api.KV{
+							Name:  "TRUE",
+							Value: "false",
+						},
+					},
+					{
+						Ptype: "web",
+						KV: api.KV{
+							Name:  "DEBUG",
+							Value: "true",
+						},
+					},
 				},
 			}, r)
 		}
@@ -169,13 +114,18 @@ func TestConfigSet(t *testing.T) {
 		fmt.Fprintf(w, `{
 	"owner": "jkirk",
 	"app": "foo",
-	"values": {
-			"TEST":  "testing",
-			"NCC":   "1701",
-			"TRUE":  "false",
-			"SSH_KEY": "LS0tLS1CRUdJTiBPUEVOU1NIIFBSSVZBVEUgS0VZLS0tLS0=",
-			"FLOAT": "12.34"
-	},
+	"values": [
+	  {
+	    "ptype": "web",
+	    "name": "TRUE",
+		"value": "false"
+	  },
+	  {
+	    "ptype": "web",
+	    "name": "DEBUG",
+		"value": "true"
+	  }
+	],
 	"memory": {},
 	"cpu": {},
 	"tags": {},
@@ -189,17 +139,11 @@ func TestConfigSet(t *testing.T) {
 	var b bytes.Buffer
 	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
 
-	err = cmdr.ConfigSet("foo", "", []string{"TRUE=false", "SSH_KEY=-----BEGIN OPENSSH PRIVATE KEY-----"}, "yes")
+	err = cmdr.ConfigSet("foo", "web", "", []string{"TRUE=false", "DEBUG=true"}, "yes")
 	assert.NoError(t, err)
 
 	assert.Equal(t, testutil.StripProgress(b.String()), `Creating config... done
 
-PTYPE    NAME       VALUE                                            
-N/A      FLOAT      12.34                                               
-N/A      NCC        1701                                                
-N/A      SSH_KEY    LS0tLS1CRUdJTiBPUEVOU1NIIFBSSVZBVEUgS0VZLS0tLS0=    
-N/A      TEST       testing                                             
-N/A      TRUE       false                                               
 `, "output")
 }
 
@@ -215,62 +159,13 @@ func TestConfigUnset(t *testing.T) {
 		testutil.SetHeaders(w)
 		if r.Method == "POST" {
 			testutil.AssertBody(t, api.Config{
-				Values: map[string]interface{}{
-					"FOO": nil,
-				},
-			}, r)
-		}
-
-		fmt.Fprintf(w, `{
-	"owner": "jkirk",
-	"app": "foo",
-	"values": {
-			"TEST":  "testing",
-			"NCC":   "1701",
-			"TRUE":  "false",
-			"FLOAT": "12.34"
-	},
-	"memory": {},
-	"cpu": {},
-	"tags": {},
-	"registry": {},
-	"created": "2014-01-01T00:00:00UTC",
-	"updated": "2014-01-01T00:00:00UTC",
-	"uuid": "de1bf5b5-4a72-4f94-a10c-d2a3741cdf75"
-}`)
-	})
-
-	var b bytes.Buffer
-	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
-
-	err = cmdr.ConfigUnset("foo", "", []string{"FOO"}, "yes")
-	assert.NoError(t, err)
-
-	assert.Equal(t, testutil.StripProgress(b.String()), `Removing config... done
-
-PTYPE    NAME     VALUE   
-N/A      FLOAT    12.34      
-N/A      NCC      1701       
-N/A      TEST     testing    
-N/A      TRUE     false      
-`, "output")
-}
-
-func TestConfigUnsetTypedValues(t *testing.T) {
-	t.Parallel()
-	cf, server, err := testutil.NewTestServerAndClient()
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer server.Close()
-
-	server.Mux.HandleFunc("/v2/apps/foo/config/", func(w http.ResponseWriter, r *http.Request) {
-		testutil.SetHeaders(w)
-		if r.Method == "POST" {
-			testutil.AssertBody(t, api.Config{
-				TypedValues: map[string]api.ConfigValues{
-					"web": {
-						"FOO": nil,
+				Values: []api.ConfigValue{
+					{
+						Ptype: "web",
+						KV: api.KV{
+							Name:  "FOO",
+							Value: "",
+						},
 					},
 				},
 			}, r)
@@ -279,17 +174,18 @@ func TestConfigUnsetTypedValues(t *testing.T) {
 		fmt.Fprintf(w, `{
 	"owner": "jkirk",
 	"app": "foo",
-	"values": {
-		"RELEASE_VERSION": "v1"
-	},
-	"typed_values": {
-		"web": {
-			"TEST":  "testing",
-			"NCC":   "1701",
-			"TRUE":  "false",
-			"FLOAT": "12.34"
-		}
-	},
+	"values": [
+	  {
+	    "ptype": "web",
+	    "name": "FLOAT",
+		"value": "12.34"
+	  },
+	  {
+	    "ptype": "web",
+	    "name": "NCC",
+		"value": "1701"
+	  }
+	],
 	"memory": {},
 	"cpu": {},
 	"tags": {},
@@ -303,16 +199,10 @@ func TestConfigUnsetTypedValues(t *testing.T) {
 	var b bytes.Buffer
 	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
 
-	err = cmdr.ConfigUnset("foo", "web", []string{"FOO"}, "")
+	err = cmdr.ConfigUnset("foo", "web", "", []string{"FOO"}, "yes")
 	assert.NoError(t, err)
 
 	assert.Equal(t, testutil.StripProgress(b.String()), `Removing config... done
 
-PTYPE    NAME               VALUE   
-N/A      RELEASE_VERSION    v1         
-web      FLOAT              12.34      
-web      NCC                1701       
-web      TEST               testing    
-web      TRUE               false      
 `, "output")
 }
diff --git a/cmd/healthchecks_test.go b/cmd/healthchecks_test.go
index 20e0b49c..79e2e5ab 100644
--- a/cmd/healthchecks_test.go
+++ b/cmd/healthchecks_test.go
@@ -27,7 +27,7 @@ func TestHealthchecksList(t *testing.T) {
   "uuid": "c039a380-6068-4511-b35a-535a73b86ef5",
   "app": "foo",
   "owner": "bar",
-  "values": {},
+  "values": [],
   "memory": {},
   "cpu": {},
   "tags": {},
@@ -81,7 +81,7 @@ func TestHealthchecksListNoHealthCheck(t *testing.T) {
   "uuid": "c039a380-6068-4511-b35a-535a73b86ef5",
   "app": "foo",
   "owner": "bar",
-  "values": {},
+  "values": [],
   "memory": {},
   "cpu": {},
   "tags": {},
@@ -115,7 +115,7 @@ func TestHealthchecksListAllHealthChecks(t *testing.T) {
   "uuid": "c039a380-6068-4511-b35a-535a73b86ef5",
   "app": "foo",
   "owner": "bar",
-  "values": {},
+  "values": [],
   "memory": {},
   "cpu": {},
   "tags": {},
@@ -183,7 +183,7 @@ func TestHealthchecksSet(t *testing.T) {
   "uuid": "c039a380-6068-4511-b35a-535a73b86ef5",
   "app": "foo",
   "owner": "bar",
-  "values": {},
+  "values": [],
   "memory": {},
   "cpu": {},
   "tags": {},
@@ -238,7 +238,7 @@ func TestHealthchecksUnset(t *testing.T) {
   "uuid": "c039a380-6068-4511-b35a-535a73b86ef5",
   "app": "foo",
   "owner": "bar",
-  "values": {},
+  "values": [],
   "memory": {},
   "cpu": {},
   "tags": {},
diff --git a/cmd/limits_test.go b/cmd/limits_test.go
index fb07a2e4..e85e25a8 100644
--- a/cmd/limits_test.go
+++ b/cmd/limits_test.go
@@ -280,7 +280,7 @@ func TestLimitsList(t *testing.T) {
 		fmt.Fprintf(w, `{
 			"owner": "jkirk",
 			"app": "enterprise",
-			"values": {},
+			"values": [],
 			"limits": {
 				"web": "std1.large.c1m1",
 				"worker": "std1.large.c1m1",
@@ -345,7 +345,7 @@ func TestLimitsSet(t *testing.T) {
 		fmt.Fprintf(w, `{
 			"owner": "jkirk",
 			"app": "foo",
-			"values": {},
+			"values": [],
 			"limits": {"web": "std1.large.c1m1"},
 			"tags": {},
 			"registry": {},
@@ -380,7 +380,7 @@ web      std1.large.c1m1    1        1 GiB     Unknown Integrated GPU shared * 1
 		fmt.Fprintf(w, `{
 			"owner": "bedison",
 			"app": "franklin",
-			"values": {},
+			"values": [],
 			"limits": {
 				"web": "std1.large.c1m1"
 			},
@@ -418,7 +418,7 @@ web      std1.large.c1m1    1        1 GiB     Unknown Integrated GPU shared * 1
 		fmt.Fprintf(w, `{
 			"owner": "foo",
 			"app": "jim",
-			"values": {},
+			"values": [],
 			"limits": {
 				"web": "std1.large.c1m1",
 				"worker": "std1.large.c1m1",
@@ -460,7 +460,7 @@ worker    std1.large.c1m1    1        1 GiB     Unknown Integrated GPU shared *
 		fmt.Fprintf(w, `{
 			"owner": "foo",
 			"app": "jim",
-			"values": {},
+			"values": [],
 			"limits": {
 				"web": "std1.large.c1m1",
 				"worker": "std1.large.c1m1",
@@ -505,7 +505,7 @@ func TestLimitsUnset(t *testing.T) {
 		fmt.Fprintf(w, `{
 			"owner": "jkirk",
 			"app": "foo",
-			"values": {},
+			"values": [],
 			"limits": {
 				"web": "std1.large.c1m1"
 			},
@@ -542,7 +542,7 @@ web      std1.large.c1m1    1        1 GiB     Unknown Integrated GPU shared * 1
 		fmt.Fprintf(w, `{
 			"owner": "bedison",
 			"app": "franklin",
-			"values": {},
+			"values": [],
 			"limits": {},
 			"tags": {},
 			"registry": {},
diff --git a/cmd/pts.go b/cmd/pts.go
index 8a42d8c4..345605e9 100644
--- a/cmd/pts.go
+++ b/cmd/pts.go
@@ -116,13 +116,37 @@ func (d *DryccCmd) PtsRestart(appID string, targets []string, confirm string) er
 	return nil
 }
 
+// PtsRestart restarts an app's processes.
+func (d *DryccCmd) PtsClean(appID string, targets []string) error {
+	s, appID, err := load(d.ConfigFile, appID)
+	if err != nil {
+		return err
+	}
+	d.Printf("Cleaning process types... but first, %s!\n", drinkOfChoice())
+	startTime := time.Now()
+	quit := progress(d.WOut)
+	ptypes := strings.Join(targets, ",")
+	targetMap := map[string]string{
+		"ptypes": ptypes,
+	}
+	err = pts.Clean(s.Client, appID, targetMap)
+	quit <- true
+	<-quit
+	if err != nil {
+		return err
+	}
+
+	d.Printf("done in %ds\n", int(time.Since(startTime).Seconds()))
+	return nil
+}
+
 func printProcessTypes(d *DryccCmd, appID string, ptypes api.Ptypes) {
 	pts := pts.ByType(ptypes)
 
 	if len(pts) == 0 {
 		d.Println(fmt.Sprintf("No processes found in %s app.", appID))
 	} else {
-		table := d.getDefaultFormatTable([]string{"NAME", "RELEASE", "READY", "UP-TO-DATE", "AVAILABLE", "STARTED"})
+		table := d.getDefaultFormatTable([]string{"NAME", "RELEASE", "READY", "UP-TO-DATE", "AVAILABLE", "STARTED", "GARBAGE"})
 		for _, pt := range pts {
 			table.Append([]string{
 				pt.Name,
@@ -131,6 +155,7 @@ func printProcessTypes(d *DryccCmd, appID string, ptypes api.Ptypes) {
 				fmt.Sprintf("%v", pt.UpToDate),
 				fmt.Sprintf("%v", pt.AvailableReplicas),
 				d.formatTime(pt.Started),
+				fmt.Sprintf("%t", pt.Garbage),
 			})
 		}
 		table.Render()
diff --git a/cmd/pts_test.go b/cmd/pts_test.go
index 8bbd5bc4..c35e2b17 100644
--- a/cmd/pts_test.go
+++ b/cmd/pts_test.go
@@ -21,6 +21,7 @@ func TestPrintProcessTypes(t *testing.T) {
 			UpToDate:          1,
 			AvailableReplicas: 1,
 			Started:           "2024-07-04T14:33:00CST",
+			Garbage:           false,
 		},
 		{
 			Name:              "worker",
@@ -29,6 +30,7 @@ func TestPrintProcessTypes(t *testing.T) {
 			UpToDate:          1,
 			AvailableReplicas: 1,
 			Started:           "2024-07-04T14:33:00CST",
+			Garbage:           false,
 		},
 	}
 	cf, server, err := testutil.NewTestServerAndClient()
@@ -39,9 +41,9 @@ func TestPrintProcessTypes(t *testing.T) {
 
 	printProcessTypes(&DryccCmd{WOut: &b, ConfigFile: cf}, "appname", ptypes)
 
-	assert.Equal(t, b.String(), `NAME      RELEASE    READY    UP-TO-DATE    AVAILABLE    STARTED                
-web       v1         1/1      1             1            2024-07-04T14:33:00CST    
-worker    v1         1/1      1             1            2024-07-04T14:33:00CST    
+	assert.Equal(t, b.String(), `NAME      RELEASE    READY    UP-TO-DATE    AVAILABLE    STARTED                   GARBAGE 
+web       v1         1/1      1             1            2024-07-04T14:33:00CST    false      
+worker    v1         1/1      1             1            2024-07-04T14:33:00CST    false      
 `, "output")
 }
 
@@ -68,7 +70,8 @@ func TestPtsList(t *testing.T) {
 					"ready": "1/1",
 					"up_to_date": 1,
 					"available_replicas": 1,
-					"started": "2016-02-13T00:47:52"
+					"started": "2016-02-13T00:47:52",
+					"garbage": false
 				}
 			]
 		}`)
@@ -77,8 +80,8 @@ func TestPtsList(t *testing.T) {
 	err = cmdr.PtsList("foo", -1)
 	assert.NoError(t, err)
 
-	assert.Equal(t, b.String(), `NAME    RELEASE    READY    UP-TO-DATE    AVAILABLE    STARTED             
-web     v1         1/1      1             1            2016-02-13T00:47:52    
+	assert.Equal(t, b.String(), `NAME    RELEASE    READY    UP-TO-DATE    AVAILABLE    STARTED                GARBAGE 
+web     v1         1/1      1             1            2016-02-13T00:47:52    false      
 `, "output")
 }
 
@@ -241,3 +244,25 @@ func TestPtsDescribe(t *testing.T) {
 	err = cmdr.PtsDescribe("foo", "web")
 	assert.NoError(t, err)
 }
+
+func TestPtsClean(t *testing.T) {
+	t.Parallel()
+	cf, server, err := testutil.NewTestServerAndClient()
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer server.Close()
+	var b bytes.Buffer
+	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
+
+	server.Mux.HandleFunc("/v2/apps/testapp/ptypes/clean/", func(w http.ResponseWriter, r *http.Request) {
+		testutil.AssertBody(t, map[string]string{"ptypes": "worker"}, r)
+		testutil.SetHeaders(w)
+		w.WriteHeader(http.StatusNoContent)
+	})
+
+	b.Reset()
+
+	err = cmdr.PtsClean("testapp", []string{"worker"})
+	assert.NoError(t, err)
+}
diff --git a/cmd/registry.go b/cmd/registry.go
index 1cbac752..e3fe8b20 100644
--- a/cmd/registry.go
+++ b/cmd/registry.go
@@ -2,14 +2,13 @@ package cmd
 
 import (
 	"fmt"
-	"strings"
 
 	"github.com/drycc/controller-sdk-go/api"
 	"github.com/drycc/controller-sdk-go/config"
 )
 
 // RegistryList lists an app's registry information.
-func (d *DryccCmd) RegistryList(appID string, version int) error {
+func (d *DryccCmd) RegistryList(appID, ptype string, version int) error {
 	s, appID, err := load(d.ConfigFile, appID)
 
 	if err != nil {
@@ -22,37 +21,53 @@ func (d *DryccCmd) RegistryList(appID string, version int) error {
 	}
 	if len(config.Registry) == 0 {
 		d.Println(fmt.Sprintf("No registrys found in %s app.", appID))
+		return nil
+	}
+	ptypes := []string{}
+	if ptype != "" {
+		ptypes = append(ptypes, ptype)
+
 	} else {
-		table := d.getDefaultFormatTable([]string{"KEY", "VALUE"})
-		for _, key := range *sortKeys(config.Registry) {
+		for ptype := range config.Registry {
+			ptypes = append(ptypes, ptype)
+		}
+	}
+
+	table := d.getDefaultFormatTable([]string{"PTYPE", "USERNAME", "PASSWORD"})
+	for _, ptype := range sortPtypes(ptypes) {
+		if config.Registry[ptype]["username"] != nil {
 			table.Append([]string{
-				key,
-				fmt.Sprintf("%v", config.Registry[key]),
+				ptype,
+				fmt.Sprintf("%v", config.Registry[ptype]["username"]),
+				fmt.Sprintf("%v", config.Registry[ptype]["password"]),
 			})
 		}
-		table.Render()
 	}
+
+	table.Render()
+
 	return nil
 }
 
 // RegistrySet sets an app's registry information.
-func (d *DryccCmd) RegistrySet(appID string, item []string) error {
+func (d *DryccCmd) RegistrySet(appID, ptype, username, password string) error {
 	s, appID, err := load(d.ConfigFile, appID)
 
 	if err != nil {
 		return err
 	}
 
-	registryMap, err := parseInfos(item)
-	if err != nil {
-		return err
-	}
-
 	d.Print("Applying registry information... ")
 
 	quit := progress(d.WOut)
+
 	configObj := api.Config{}
-	configObj.Registry = registryMap
+	registry := make(map[string]map[string]interface{})
+	registry[ptype] = map[string]interface{}{
+		"username": username,
+		"password": password,
+	}
+	configObj.Registry = registry
 
 	_, err = config.Set(s.Client, appID, configObj)
 	quit <- true
@@ -63,11 +78,11 @@ func (d *DryccCmd) RegistrySet(appID string, item []string) error {
 
 	d.Print("done\n\n")
 
-	return d.RegistryList(appID, -1)
+	return d.RegistryList(appID, ptype, -1)
 }
 
 // RegistryUnset removes an app's registry information.
-func (d *DryccCmd) RegistryUnset(appID string, items []string) error {
+func (d *DryccCmd) RegistryUnset(appID, ptype string) error {
 	s, appID, err := load(d.ConfigFile, appID)
 
 	if err != nil {
@@ -79,15 +94,12 @@ func (d *DryccCmd) RegistryUnset(appID string, items []string) error {
 	quit := progress(d.WOut)
 
 	configObj := api.Config{}
-
-	registryMap := make(map[string]interface{})
-
-	for _, key := range items {
-		registryMap[key] = nil
+	registry := make(map[string]map[string]interface{})
+	registry[ptype] = map[string]interface{}{
+		"username": nil,
+		"password": nil,
 	}
-
-	configObj.Registry = registryMap
-
+	configObj.Registry = registry
 	_, err = config.Set(s.Client, appID, configObj)
 	quit <- true
 	<-quit
@@ -97,36 +109,5 @@ func (d *DryccCmd) RegistryUnset(appID string, items []string) error {
 
 	d.Print("done\n\n")
 
-	return d.RegistryList(appID, -1)
-}
-
-func parseInfos(items []string) (map[string]interface{}, error) {
-	registryMap := make(map[string]interface{})
-
-	for _, item := range items {
-		key, value, err := parseInfo(item)
-
-		if err != nil {
-			return nil, err
-		}
-
-		registryMap[key] = value
-	}
-
-	return registryMap, nil
-}
-
-func parseInfo(item string) (string, string, error) {
-	parts := strings.SplitN(item, "=", 2)
-
-	if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
-		return "", "", fmt.Errorf(`%s is invalid. Must be in format key=value
-Examples: username=bob password=s3cur3pw1`, item)
-	}
-
-	if parts[0] != "username" && parts[0] != "password" {
-		return "", "", fmt.Errorf(`%s is invalid. Valid keys are "username" or "password"`, parts[0])
-	}
-
-	return parts[0], parts[1], nil
+	return nil
 }
diff --git a/cmd/registry_test.go b/cmd/registry_test.go
index 8b3b76c8..2977528c 100644
--- a/cmd/registry_test.go
+++ b/cmd/registry_test.go
@@ -11,68 +11,6 @@ import (
 	"github.com/stretchr/testify/assert"
 )
 
-type parseInfoCase struct {
-	Input         string
-	Key           string
-	Value         string
-	ExpectedError bool
-	ExpectedMsg   string
-}
-
-func TestParseInfo(t *testing.T) {
-	t.Parallel()
-
-	cases := []parseInfoCase{
-		{"username=test", "username", "test", false, ""},
-		{"password=test=", "password", "test=", false, ""},
-		{"test=1", "", "", true, `test is invalid. Valid keys are "username" or "password"`},
-		{"test", "", "", true, `test is invalid. Must be in format key=value
-Examples: username=bob password=s3cur3pw1`},
-		{"test=", "", "", true, `test= is invalid. Must be in format key=value
-Examples: username=bob password=s3cur3pw1`},
-		{"=test", "", "", true, `=test is invalid. Must be in format key=value
-Examples: username=bob password=s3cur3pw1`},
-	}
-
-	for _, check := range cases {
-		key, value, err := parseInfo(check.Input)
-		if check.ExpectedError {
-			assert.Equal(t, err.Error(), check.ExpectedMsg, "error")
-		} else {
-			assert.NoError(t, err)
-			assert.Equal(t, key, check.Key, "key")
-			assert.Equal(t, value, check.Value, "value")
-		}
-	}
-}
-
-type parseInfosCase struct {
-	Input         []string
-	ExpectedMap   map[string]interface{}
-	ExpectedError bool
-	ExpectedMsg   string
-}
-
-func TestParseInfos(t *testing.T) {
-	t.Parallel()
-
-	cases := []parseInfosCase{
-		{[]string{"username=test", "password=abc123"}, map[string]interface{}{"username": "test", "password": "abc123"}, false, ""},
-		{[]string{"foo=", "true=false"}, nil, true, `foo= is invalid. Must be in format key=value
-Examples: username=bob password=s3cur3pw1`},
-	}
-
-	for _, check := range cases {
-		actual, err := parseInfos(check.Input)
-		if check.ExpectedError {
-			assert.Equal(t, err.Error(), check.ExpectedMsg, "error")
-		} else {
-			assert.NoError(t, err)
-			assert.Equal(t, actual, check.ExpectedMap, "map")
-		}
-	}
-}
-
 func TestRegistryList(t *testing.T) {
 	t.Parallel()
 	cf, server, err := testutil.NewTestServerAndClient()
@@ -86,14 +24,15 @@ func TestRegistryList(t *testing.T) {
 		fmt.Fprintf(w, `{
 			"owner": "jkirk",
 			"app": "enterprise",
-			"values": {},
+			"values": [],
 			"memory": {},
 			"cpu": {},
 			"tags": {},
 			"registry": {
+			  "web": {
 				"username": "jkirk",
 				"password": "ncc1701"
-			},
+			}},
 			"created": "2014-01-01T00:00:00UTC",
 			"updated": "2014-01-01T00:00:00UTC",
 			"uuid": "de1bf5b5-4a72-4f94-a10c-d2a3741cdf75"
@@ -103,11 +42,10 @@ func TestRegistryList(t *testing.T) {
 	var b bytes.Buffer
 	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
 
-	err = cmdr.RegistryList("enterprise", -1)
+	err = cmdr.RegistryList("enterprise", "web", -1)
 	assert.NoError(t, err)
-	assert.Equal(t, b.String(), `KEY         VALUE   
-password    ncc1701    
-username    jkirk      
+	assert.Equal(t, b.String(), `PTYPE    USERNAME    PASSWORD 
+web      jkirk       ncc1701     
 `, "output")
 }
 
@@ -123,9 +61,11 @@ func TestRegistrySet(t *testing.T) {
 		testutil.SetHeaders(w)
 		if r.Method == "POST" {
 			testutil.AssertBody(t, api.Config{
-				Registry: map[string]interface{}{
-					"username": "jkirk",
-					"password": "ncc1701",
+				Registry: map[string]map[string]interface{}{
+					"web": {
+						"username": "jkirk",
+						"password": "ncc1701",
+					},
 				},
 			}, r)
 		}
@@ -133,12 +73,14 @@ func TestRegistrySet(t *testing.T) {
 		fmt.Fprintf(w, `{
 			"owner": "jkirk",
 			"app": "foo",
-			"values": {},
+			"values": [],
 			"memory": {},
 			"cpu": {},
 			"registry": {
-				"username": "jkirk",
+			  "web": {
+			  	"username": "jkirk",
 				"password": "ncc1701"
+			  }
 			},
 			"registry": {},
 			"created": "2014-01-01T00:00:00UTC",
@@ -150,14 +92,13 @@ func TestRegistrySet(t *testing.T) {
 	var b bytes.Buffer
 	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
 
-	err = cmdr.RegistrySet("foo", []string{"username=jkirk", "password=ncc1701"})
+	err = cmdr.RegistrySet("foo", "web", "jkirk", "ncc1701")
 	assert.NoError(t, err)
 
 	assert.Equal(t, testutil.StripProgress(b.String()), `Applying registry information... done
 
-KEY         VALUE   
-password    ncc1701    
-username    jkirk      
+PTYPE    USERNAME    PASSWORD 
+web      jkirk       ncc1701     
 `, "output")
 }
 
@@ -173,9 +114,11 @@ func TestRegistryUnset(t *testing.T) {
 		testutil.SetHeaders(w)
 		if r.Method == "POST" {
 			testutil.AssertBody(t, api.Config{
-				Registry: map[string]interface{}{
-					"username": nil,
-					"password": nil,
+				Registry: map[string]map[string]interface{}{
+					"web": {
+						"username": nil,
+						"password": nil,
+					},
 				},
 			}, r)
 		}
@@ -183,7 +126,7 @@ func TestRegistryUnset(t *testing.T) {
 		fmt.Fprintf(w, `{
 			"owner": "jkirk",
 			"app": "foo",
-			"values": {},
+			"values": [],
 			"memory": {},
 			"cpu": {},
 			"tags": {},
@@ -197,11 +140,10 @@ func TestRegistryUnset(t *testing.T) {
 	var b bytes.Buffer
 	cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
 
-	err = cmdr.RegistryUnset("foo", []string{"username", "password"})
+	err = cmdr.RegistryUnset("foo", "web")
 	assert.NoError(t, err)
 
 	assert.Equal(t, testutil.StripProgress(b.String()), `Applying registry information... done
 
-No registrys found in foo app.
 `, "output")
 }
diff --git a/cmd/resources.go b/cmd/resources.go
index 4e53f01b..00f89233 100644
--- a/cmd/resources.go
+++ b/cmd/resources.go
@@ -164,7 +164,7 @@ func (d *DryccCmd) ResourcesList(appID string, results int) error {
 }
 
 // ResourceGet describe a resource from the application
-func (d *DryccCmd) ResourceGet(appID, name string) error {
+func (d *DryccCmd) ResourceGet(appID, name string, details bool) error {
 	s, appID, err := load(d.ConfigFile, appID)
 
 	if err != nil {
@@ -189,11 +189,13 @@ func (d *DryccCmd) ResourceGet(appID, name string) error {
 	for _, key := range *sortKeys(resource.Data) {
 		table.Append([]string{"", fmt.Sprintf("%s:", key), fmt.Sprintf("%s", resource.Data[key])})
 	}
-	table.Append([]string{"Options:"})
-	for _, key := range *sortKeys(resource.Options) {
-		table.Append([]string{"", fmt.Sprintf("%s:", key), fmt.Sprintf("%s", resource.Options[key])})
+	if details {
+		table.Append([]string{"Options:"})
+		for _, key := range *sortKeys(resource.Options) {
+			table.Append([]string{"", fmt.Sprintf("%s:", key), fmt.Sprintf("%s", resource.Options[key])})
+		}
+		table.Append([]string{"Message:", d.wrapString(safeGetString(resource.Message))})
 	}
-	table.Append([]string{"Message:", d.wrapString(safeGetString(resource.Message))})
 	table.Append([]string{"Created:", d.formatTime(resource.Created)})
 	table.Append([]string{"Updated:", d.formatTime(resource.Updated)})
 	table.Render()
diff --git a/cmd/resources_test.go b/cmd/resources_test.go
index 1631cb5f..27537720 100644
--- a/cmd/resources_test.go
+++ b/cmd/resources_test.go
@@ -167,7 +167,7 @@ func TestResourceGet(t *testing.T) {
 }`)
 	})
 
-	err = cmdr.ResourceGet("example-go", "mysql")
+	err = cmdr.ResourceGet("example-go", "mysql", true)
 	assert.NoError(t, err)
 	// todo format data json to yaml
 	assert.Equal(t, b.String(), `App:        example-go                              
diff --git a/cmd/tags_test.go b/cmd/tags_test.go
index c2e8b060..d2f20556 100644
--- a/cmd/tags_test.go
+++ b/cmd/tags_test.go
@@ -87,7 +87,7 @@ func TestTagsList(t *testing.T) {
 		fmt.Fprintf(w, `{
 			"owner": "jkirk",
 			"app": "enterprise",
-			"values": {},
+			"values": [],
 			"memory": {},
 			"cpu": {},
 			"tags": {
@@ -135,7 +135,7 @@ func TestTagsSet(t *testing.T) {
 		fmt.Fprintf(w, `{
 			"owner": "jkirk",
 			"app": "foo",
-			"values": {},
+			"values": [],
 			"memory": {},
 			"cpu": {},
 			"tags": {
@@ -184,7 +184,7 @@ func TestTagsUnset(t *testing.T) {
 		fmt.Fprintf(w, `{
 			"owner": "jkirk",
 			"app": "foo",
-			"values": {},
+			"values": [],
 			"memory": {},
 			"cpu": {},
 			"tags": {
diff --git a/cmd/timeouts_test.go b/cmd/timeouts_test.go
index 1b0c8785..bfacef3b 100644
--- a/cmd/timeouts_test.go
+++ b/cmd/timeouts_test.go
@@ -85,7 +85,7 @@ func TestTimeoutsList(t *testing.T) {
 		fmt.Fprintf(w, `{
       "owner": "jkirk",
       "app": "enterprise",
-      "values": {},
+      "values": [],
       "memory": {},
       "cpu": {},
       "tags": {},
@@ -115,7 +115,7 @@ worker    20
 		fmt.Fprintf(w, `{
       "owner": "bedison",
       "app": "franklin",
-      "values": {},
+      "values": [],
       "memory": {},
       "cpu": {},
       "tags": {},
@@ -154,7 +154,7 @@ func TestTimeoutsSet(t *testing.T) {
 		fmt.Fprintf(w, `{
       "owner": "jkirk",
       "app": "foo",
-      "values": {},
+      "values": [],
       "memory": {},
       "cpu": {},
       "termination_grace_period": {
@@ -193,7 +193,7 @@ web      10
 		fmt.Fprintf(w, `{
       "owner": "bedison",
       "app": "franklin",
-      "values": {},
+      "values": [],
       "memory": {},
       "cpu": {},
       "termination_grace_period": {
@@ -233,7 +233,7 @@ web      10
 		fmt.Fprintf(w, `{
       "owner": "foo",
       "app": "jim",
-      "values": {},
+      "values": [],
       "memory": {},
       "cpu": {},
       "termination_grace_period": {
@@ -284,7 +284,7 @@ func TestTimeoutsUnset(t *testing.T) {
 		fmt.Fprintf(w, `{
       "owner": "jkirk",
       "app": "foo",
-      "values": {},
+      "values": [],
       "memory": {},
       "cpu": {},
       "termination_grace_period": {
@@ -323,7 +323,7 @@ web      10
 		fmt.Fprintf(w, `{
       "owner": "bedison",
       "app": "franklin",
-      "values": {},
+      "values": [],
       "memory": {},
       "cpu": {},
       "termination_grace_period": {
diff --git a/cmd/utils.go b/cmd/utils.go
index f6f695d3..bdf02ec9 100644
--- a/cmd/utils.go
+++ b/cmd/utils.go
@@ -11,6 +11,7 @@ import (
 	"time"
 
 	drycc "github.com/drycc/controller-sdk-go"
+	"github.com/drycc/controller-sdk-go/api"
 	"github.com/drycc/workflow-cli/pkg/git"
 	"github.com/drycc/workflow-cli/settings"
 	"github.com/olekukonko/tablewriter"
@@ -179,6 +180,26 @@ func sortKeys(data map[string]interface{}) *[]string {
 	return &keys
 }
 
+func sortConfigValues(data []api.ConfigValue) []api.ConfigValue {
+	sort.Slice(data, func(i, j int) bool {
+		return strings.ToLower(data[i].Name) < strings.ToLower(data[j].Name)
+	})
+	return data
+}
+
+func sortPtypes(ptypes []string) []string {
+	sort.Slice(ptypes, func(i, j int) bool {
+		if ptypes[i] == "web" {
+			return true
+		}
+		if ptypes[j] == "web" {
+			return false
+		}
+		return ptypes[i] < ptypes[j]
+	})
+	return ptypes
+}
+
 func safeGetString(data string) string {
 	if data == "" {
 		return "<none>"
diff --git a/cmd/volumes_test.go b/cmd/volumes_test.go
index 396eafef..f46c08f0 100644
--- a/cmd/volumes_test.go
+++ b/cmd/volumes_test.go
@@ -170,7 +170,7 @@ func TestVolumesClientCp(t *testing.T) {
 			fmt.Fprintf(w, `{"results":[{"name":"hello.txt","size":"4159","timestamp":"2024-06-25T22:55:16+08:00","type":"file","path":"/hello.txt"}], "count": 1}`)
 		}
 	})
-	server.Mux.HandleFunc("/v2/apps/example-go/volumes/myvolume/client/hello.txt", func(w http.ResponseWriter, r *http.Request) {
+	server.Mux.HandleFunc("/v2/apps/example-go/volumes/myvolume/client/hello.txt", func(w http.ResponseWriter, _ *http.Request) {
 		testutil.SetHeaders(w)
 		fmt.Fprintf(w, `hello word`)
 	})
diff --git a/go.mod b/go.mod
index 6f542aff..622a2c2b 100644
--- a/go.mod
+++ b/go.mod
@@ -5,7 +5,7 @@ go 1.23
 require (
 	github.com/containerd/console v1.0.4
 	github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815
-	github.com/drycc/controller-sdk-go v0.0.0-20241028091950-441681c47292
+	github.com/drycc/controller-sdk-go v0.0.0-20241122013249-f1d6fea5bf81
 	github.com/drycc/pkg v0.0.0-20241028091700-8caca001c932
 	github.com/minio/selfupdate v0.6.0
 	github.com/olekukonko/tablewriter v0.0.5
diff --git a/go.sum b/go.sum
index 9c3ba6f0..1479e783 100644
--- a/go.sum
+++ b/go.sum
@@ -9,8 +9,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 h1:bWDMxwH3px2JBh6AyO7hdCn/PkvCZXii8TGj7sbtEbQ=
 github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
-github.com/drycc/controller-sdk-go v0.0.0-20241028091950-441681c47292 h1:CLpq3s2NxjJs2evDvDqbTwUCDuGyEEjk6bIjn+g5H+M=
-github.com/drycc/controller-sdk-go v0.0.0-20241028091950-441681c47292/go.mod h1:F9ViuoVcZ/OG1WnfZBYH6j4v6mb5B+RW9FLaCopADoo=
+github.com/drycc/controller-sdk-go v0.0.0-20241122013249-f1d6fea5bf81 h1:y809MASIviVNxzUxP1CDU2EaB5AruRNAiFKjOMCx+0s=
+github.com/drycc/controller-sdk-go v0.0.0-20241122013249-f1d6fea5bf81/go.mod h1:F9ViuoVcZ/OG1WnfZBYH6j4v6mb5B+RW9FLaCopADoo=
 github.com/drycc/pkg v0.0.0-20241028091700-8caca001c932 h1:tUfkfjmFmlm9n0dYp1NoHcFK6+APD/wHZ2We6CKXDSI=
 github.com/drycc/pkg v0.0.0-20241028091700-8caca001c932/go.mod h1:upGcZF/RCEqidGjSnWHh/txRC1I4GPsRGvF8/+bDfwU=
 github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
diff --git a/parser/auth.go b/parser/auth.go
index 8358b426..d284998a 100644
--- a/parser/auth.go
+++ b/parser/auth.go
@@ -46,9 +46,9 @@ Arguments:
     a fully-qualified controller URI, e.g. "http://drycc.local3.dryccapp.com/".
 
 Options:
-  --username=<username>
+  -u --username=<username>
     provide a username for the account.
-  --password=<password>
+  -p --password=<password>
     provide a password for the account.
   --ssl-verify=true
     enables/disables SSL certificate verification for API requests
diff --git a/parser/builds.go b/parser/builds.go
index 63599eec..2e4efb8b 100644
--- a/parser/builds.go
+++ b/parser/builds.go
@@ -45,7 +45,7 @@ Usage: drycc builds:info [options]
 Options:
   -a --app=<app>
     the uniquely identifiable name for the application.
-  --version=<version>
+  -v --version=<version>
     the version for which the build info needs to be displayed.
 `
 
diff --git a/parser/config.go b/parser/config.go
index 0069ca27..2b510b0d 100644
--- a/parser/config.go
+++ b/parser/config.go
@@ -10,18 +10,20 @@ func Config(argv []string, cmdr cmd.Commander) error {
 	usage := `
 Valid commands for config:
 
-config:list        list environment variables for an app
+config:info        an app config info
 config:set         set environment variables for an app
 config:unset       unset environment variables for an app
 config:pull        pull environment variables to the path
 config:push        push environment variables from the path
+config:attach      selects a environment groups to attach an app ptype
+config:detach      selects a environment groups to detach an app ptype
 
 Use 'drycc help [command]' to learn more.
 `
 
 	switch argv[0] {
-	case "config:list":
-		return configList(argv, cmdr)
+	case "config:info":
+		return configInfo(argv, cmdr)
 	case "config:set":
 		return configSet(argv, cmdr)
 	case "config:unset":
@@ -30,14 +32,18 @@ Use 'drycc help [command]' to learn more.
 		return configPull(argv, cmdr)
 	case "config:push":
 		return configPush(argv, cmdr)
+	case "config:attach":
+		return configAttach(argv, cmdr)
+	case "config:detach":
+		return configDetach(argv, cmdr)
 	default:
 		if printHelp(argv, usage) {
 			return nil
 		}
 
 		if argv[0] == "config" {
-			argv[0] = "config:list"
-			return configList(argv, cmdr)
+			argv[0] = "config:info"
+			return configInfo(argv, cmdr)
 		}
 
 		PrintUsage(cmdr)
@@ -45,18 +51,20 @@ Use 'drycc help [command]' to learn more.
 	}
 }
 
-func configList(argv []string, cmdr cmd.Commander) error {
+func configInfo(argv []string, cmdr cmd.Commander) error {
 	usage := `
-Lists environment variables for an application.
+An app config info.
 
-Usage: drycc config:list [options]
+Usage: drycc config:info [options]
 
 Options:
   -a --app=<app>
     the application that you wish to listed.
-  --ptype=<ptype>
+  -p --ptype=<ptype>
     the ptype for which the config needs to be listed.
-  --version=<version>
+  -g --group=<group>
+    the group for which the config needs to be listed.
+  -v --version=<version>
     the version for which the config needs to be listed.
 `
 
@@ -67,6 +75,7 @@ Options:
 	}
 	app := safeGetString(args, "--app")
 	ptype := safeGetString(args, "--ptype")
+	group := safeGetString(args, "--group")
 	var version int
 	if safeGetString(args, "--version") != "" {
 		if version, err = versionFromString(safeGetString(args, "--version")); err != nil {
@@ -74,12 +83,12 @@ Options:
 		}
 	}
 
-	return cmdr.ConfigList(app, ptype, version)
+	return cmdr.ConfigInfo(app, ptype, group, version)
 }
 
 func configSet(argv []string, cmdr cmd.Commander) error {
 	usage := `
-Sets environment variables for an application.
+Sets environment variables for an application or config group.
 
 Usage: drycc config:set <var>=<value> [<var>=<value>...] [options]
 
@@ -92,8 +101,10 @@ Arguments:
 Options:
   -a --app=<app>
     the application that you wish to set.
-  --ptype=<ptype>
+  -p --ptype=<ptype>
     the ptype for which the config needs to be set.
+  -g --group=<group>
+    the group for which the config needs to be set.
   --confirm=yes
     To proceed, type "yes".
 `
@@ -106,13 +117,14 @@ Options:
 
 	app := safeGetString(args, "--app")
 	ptype := safeGetString(args, "--ptype")
+	group := safeGetString(args, "--group")
 	confirm := safeGetString(args, "--confirm")
-	return cmdr.ConfigSet(app, ptype, args["<var>=<value>"].([]string), confirm)
+	return cmdr.ConfigSet(app, ptype, group, args["<var>=<value>"].([]string), confirm)
 }
 
 func configUnset(argv []string, cmdr cmd.Commander) error {
 	usage := `
-Unsets an environment variable for an application.
+Unsets an environment variable for an application or config group.
 
 Usage: drycc config:unset <key>... [options]
 
@@ -123,8 +135,10 @@ Arguments:
 Options:
   -a --app=<app>
     the application that you wish to unset.
-  --ptype=<ptype>
+  -p --ptype=<ptype>
     the ptype for which the config needs to be unset.
+  -g --group=<group>
+    the group for which the config needs to be unset.
   --confirm=yes
     To proceed, type "yes".
 `
@@ -136,13 +150,14 @@ Options:
 	}
 	app := safeGetString(args, "--app")
 	ptype := safeGetString(args, "--ptype")
+	group := safeGetString(args, "--group")
 	confirm := safeGetString(args, "--confirm")
-	return cmdr.ConfigUnset(app, ptype, args["<key>"].([]string), confirm)
+	return cmdr.ConfigUnset(app, ptype, group, args["<key>"].([]string), confirm)
 }
 
 func configPull(argv []string, cmdr cmd.Commander) error {
 	usage := `
-Extract all environment variables from an application for local use.
+Extract all environment variables from an application or config group. for local use.
 
 The environmental variables can be piped into a file, 'drycc config:pull > file',
 or stored locally in a file named .env. This file can be
@@ -153,8 +168,10 @@ Usage: drycc config:pull [options]
 Options:
   -a --app=<app>
     the application that you wish to pull.
-  --ptype=<ptype>
+  -p --ptype=<ptype>
     the ptype for which the config needs to be pull.
+  -g --group=<group>
+    the group for which the config needs to be push.
   --path=<path>
     a path leading to an environment file [default: .env]
   -i --interactive
@@ -171,16 +188,17 @@ Options:
 
 	app := safeGetString(args, "--app")
 	ptype := safeGetString(args, "--ptype")
+	group := safeGetString(args, "--group")
 	path := safeGetValue(args, "--path", ".env")
 	interactive := args["--interactive"].(bool)
 	overwrite := args["--overwrite"].(bool)
 
-	return cmdr.ConfigPull(app, ptype, path, interactive, overwrite)
+	return cmdr.ConfigPull(app, ptype, group, path, interactive, overwrite)
 }
 
 func configPush(argv []string, cmdr cmd.Commander) error {
 	usage := `
-Sets environment variables for an application.
+Sets environment variables for an application or config group..
 
 This file can be read by foreman
 to load the local environment for your app. The file should be piped via
@@ -191,8 +209,10 @@ Usage: drycc config:push [options]
 Options:
   -a --app=<app>
     the application that you wish to push.
-  --ptype=<ptype>
+  -p --ptype=<ptype>
     the ptype for which the config needs to be push.
+  -g --group=<group>
+    the group for which the config needs to be push.
   --path=<path>
     a path leading to an environment file [default: .env]
   --confirm=yes
@@ -207,7 +227,66 @@ Options:
 
 	app := safeGetString(args, "--app")
 	ptype := safeGetString(args, "--ptype")
+	group := safeGetString(args, "--group")
 	path := safeGetValue(args, "--path", ".env")
 	confirm := safeGetString(args, "--confirm")
-	return cmdr.ConfigPush(app, ptype, path, confirm)
+	return cmdr.ConfigPush(app, ptype, group, path, confirm)
+}
+
+func configAttach(argv []string, cmdr cmd.Commander) error {
+	usage := `
+Selects a environment groups to attach an app ptype.
+
+Usage: drycc config:attach <ptype> <groups> [options]
+
+Arguments:
+  <ptype>
+    the ptype that requires attach configurations.
+  <groups>
+    the config groups.
+
+Options:
+  -a --app=<app>
+    the application that you wish to attach.
+`
+
+	args, err := docopt.ParseArgs(usage, argv, "")
+
+	if err != nil {
+		return err
+	}
+
+	app := safeGetString(args, "--app")
+	ptype := safeGetString(args, "<ptype>")
+	groups := safeGetString(args, "<groups>")
+	return cmdr.ConfigAttach(app, ptype, groups)
+}
+
+func configDetach(argv []string, cmdr cmd.Commander) error {
+	usage := `
+Selects a environment groups to detach an app ptype.
+
+Usage: drycc config:detach <ptype> <groups> [options]
+
+Arguments:
+  <ptype>
+    the ptype that requires detach configurations.
+  <groups>
+    the config groups.
+
+Options:
+  -a --app=<app>
+    the application that you wish to detach.
+`
+
+	args, err := docopt.ParseArgs(usage, argv, "")
+
+	if err != nil {
+		return err
+	}
+
+	app := safeGetString(args, "--app")
+	ptype := safeGetString(args, "<ptype>")
+	groups := safeGetString(args, "<groups>")
+	return cmdr.ConfigDetach(app, ptype, groups)
 }
diff --git a/parser/config_test.go b/parser/config_test.go
index c9513e7c..f38eab58 100644
--- a/parser/config_test.go
+++ b/parser/config_test.go
@@ -12,26 +12,34 @@ import (
 // Create fake implementations of each method that return the argument
 // we expect to have called the function (as an error to satisfy the interface).
 
-func (d FakeDryccCmd) ConfigList(string, string, int) error {
-	return errors.New("config:list")
+func (d FakeDryccCmd) ConfigInfo(string, string, string, int) error {
+	return errors.New("config:info")
 }
 
-func (d FakeDryccCmd) ConfigSet(string, string, []string, string) error {
+func (d FakeDryccCmd) ConfigSet(string, string, string, []string, string) error {
 	return errors.New("config:set")
 }
 
-func (d FakeDryccCmd) ConfigUnset(string, string, []string, string) error {
+func (d FakeDryccCmd) ConfigUnset(string, string, string, []string, string) error {
 	return errors.New("config:unset")
 }
 
-func (d FakeDryccCmd) ConfigPull(string, string, string, bool, bool) error {
+func (d FakeDryccCmd) ConfigPull(string, string, string, string, bool, bool) error {
 	return errors.New("config:pull")
 }
 
-func (d FakeDryccCmd) ConfigPush(string, string, string, string) error {
+func (d FakeDryccCmd) ConfigPush(string, string, string, string, string) error {
 	return errors.New("config:push")
 }
 
+func (d FakeDryccCmd) ConfigAttach(string, string, string) error {
+	return errors.New("config:attach")
+}
+
+func (d FakeDryccCmd) ConfigDetach(string, string, string) error {
+	return errors.New("config:detach")
+}
+
 func TestConfig(t *testing.T) {
 	t.Parallel()
 
@@ -50,7 +58,7 @@ func TestConfig(t *testing.T) {
 		expected string
 	}{
 		{
-			args:     []string{"config:list"},
+			args:     []string{"config:info"},
 			expected: "",
 		},
 		{
@@ -69,9 +77,17 @@ func TestConfig(t *testing.T) {
 			args:     []string{"config:push"},
 			expected: "",
 		},
+		{
+			args:     []string{"config:attach", "web", "g1"},
+			expected: "",
+		},
+		{
+			args:     []string{"config:detach", "web", "g1"},
+			expected: "",
+		},
 		{
 			args:     []string{"config"},
-			expected: "config:list",
+			expected: "config:info",
 		},
 	}
 
diff --git a/parser/domains.go b/parser/domains.go
index dd8c059a..0513e172 100644
--- a/parser/domains.go
+++ b/parser/domains.go
@@ -50,10 +50,10 @@ Arguments:
     the domain name to be bound to the application, such as 'domain.dryccapp.com'.
 
 Options:
-  --ptype=<ptype>
-    the ptype type for domain, default[web].
   -a --app=<app>
     the uniquely identifiable name for the application.
+  -p --ptype=<ptype>
+    the ptype type for domain, default[web].
 `
 
 	args, err := docopt.ParseArgs(usage, argv, "")
diff --git a/parser/healthchecks.go b/parser/healthchecks.go
index d14e865f..b00cb8a1 100644
--- a/parser/healthchecks.go
+++ b/parser/healthchecks.go
@@ -60,9 +60,9 @@ Usage: drycc healthchecks:list [options]
 Options:
   -a --app=<app>
     the uniquely identifiable name of the application.
-  --ptype=<ptype>
+  -p --ptype=<ptype>
     the ptype for which the health check needs to be listed.
-  --version=<version>
+  -v --version=<version>
     the version for which the health check needs to be listed.
 `
 
@@ -131,10 +131,10 @@ Arguments:
 Options:
   -a --app=<app>
     the uniquely identifiable name for the application.
-  -p --path=<path>
-    the relative URL path for 'httpGet' probes. [default: /]
-  --ptype=<ptype>
+  -p --ptype=<ptype>
     the ptype for which the health check needs to be applied.
+  --path=<path>
+    the relative URL path for 'httpGet' probes. [default: /]
   --headers=<headers>...
     the HTTP headers to send for 'httpGet' probes, separated by commas.
   --initial-delay-timeout=<initial-delay-timeout>
@@ -234,7 +234,7 @@ Arguments:
 Options:
   -a --app=<app>
     the uniquely identifiable name for the application.
-  --ptype=<ptype>
+  -p --ptype=<ptype>
     the ptype for which the health check needs to be removed.
 `
 
diff --git a/parser/limits.go b/parser/limits.go
index c27f2df4..3f013d97 100644
--- a/parser/limits.go
+++ b/parser/limits.go
@@ -54,7 +54,7 @@ Usage: drycc limits:list [options]
 Options:
   -a --app=<app>
     the uniquely identifiable name of the application.
-  --version=<version>
+  -v --version=<version>
     the version for which the limit needs to be listed.
 `
 
@@ -168,9 +168,9 @@ List all available limit plans.
 Usage: drycc limits:plans [options]
 
 Options:
-  --cpu=<cpu>
+  -c --cpu=<cpu>
     query plans that meet the specified number of cpu cores.
-  --memory=<memory>
+  -m --memory=<memory>
     query plans that meet the specified memory capacity, unit GiB.
   --spec-id=<spec-id>
     query plans that meet the specified spec id, see [specs] subcommand.
diff --git a/parser/pts.go b/parser/pts.go
index 45fad1d1..4b79acbc 100644
--- a/parser/pts.go
+++ b/parser/pts.go
@@ -14,6 +14,7 @@ pts:list        list application process types
 pts:describe    print a detailed description of the selected process type
 pts:restart     restart an application or process types
 pts:scale       scale process types of replicas (e.g. web=4 worker=2)
+pts:clean       clean process types of not used
 
 Use 'drycc help [command]' to learn more.
 `
@@ -27,6 +28,8 @@ Use 'drycc help [command]' to learn more.
 		return ptsRestart(argv, cmdr)
 	case "pts:scale":
 		return ptsScale(argv, cmdr)
+	case "pts:clean":
+		return ptsClean(argv, cmdr)
 	default:
 		if printHelp(argv, usage) {
 			return nil
@@ -141,3 +144,28 @@ Options:
 	apps := safeGetString(args, "--app")
 	return cmdr.PtsScale(apps, args["<ptype>=<num>"].([]string))
 }
+
+func ptsClean(argv []string, cmdr cmd.Commander) error {
+	usage := `
+Clean process types of not used.
+
+Usage: drycc pts:clean <ptype>... [options]
+
+Arguments:
+  <ptype>
+    the process name as defined in your Procfile, such as 'web' or 'worker'.
+
+Options:
+  -a --app=<app>
+    the uniquely identifiable name for the application.
+`
+
+	args, err := docopt.ParseArgs(usage, argv, "")
+
+	if err != nil {
+		return err
+	}
+
+	apps := safeGetString(args, "--app")
+	return cmdr.PtsScale(apps, args["<ptype>"].([]string))
+}
diff --git a/parser/pts_test.go b/parser/pts_test.go
index 301fb0c2..b84b6ac5 100644
--- a/parser/pts_test.go
+++ b/parser/pts_test.go
@@ -28,6 +28,10 @@ func (d FakeDryccCmd) PtsRestart(string, []string, string) error {
 	return errors.New("pts:restart")
 }
 
+func (d FakeDryccCmd) PtsClean(string, []string) error {
+	return errors.New("pts:clean")
+}
+
 func TestPts(t *testing.T) {
 	t.Parallel()
 
@@ -61,6 +65,10 @@ func TestPts(t *testing.T) {
 			args:     []string{"pts:scale", "web", "5"},
 			expected: "",
 		},
+		{
+			args:     []string{"pts:clean", "task"},
+			expected: "",
+		},
 		{
 			args:     []string{"pts"},
 			expected: "pts:list",
diff --git a/parser/registry.go b/parser/registry.go
index 6f0a57f1..6283e481 100644
--- a/parser/registry.go
+++ b/parser/registry.go
@@ -48,7 +48,9 @@ Usage: drycc registry:list [options]
 Options:
   -a --app=<app>
     the uniquely identifiable name of the application.
-  --version=<version>
+  -p --ptype=<ptype>
+    the ptype for registry.
+  -v --version=<version>
     the version for which the registry needs to be listed.
 `
 
@@ -62,7 +64,8 @@ Options:
 			return err
 		}
 	}
-	return cmdr.RegistryList(safeGetString(args, "--app"), version)
+	ptype := safeGetValue(args, "--ptype", "")
+	return cmdr.RegistryList(safeGetString(args, "--app"), ptype, version)
 }
 
 func registrySet(argv []string, cmdr cmd.Commander) error {
@@ -70,18 +73,19 @@ func registrySet(argv []string, cmdr cmd.Commander) error {
 Sets registry information for an application. These credentials are the same as those used for
 'podmain login' to the private registry.
 
-Usage: drycc registry:set <key>=<value>... [options]
+Usage: drycc registry:set <username> <password> [options]
 
 Arguments:
-  <key>
-    the uniquely identifiable name for logging into the registry. Valid keys are "username" or
-    "password"
-  <value>
-    the value of said environment variable. For example, "bob" or "mysecretpassword"
+  <username>
+    the username of the registry.
+  <password>
+    the password of the registry.
 
 Options:
   -a --app=<app>
     the uniquely identifiable name for the application.
+  -p --ptype=<ptype>
+    the ptype for registry, default[web].
 `
 
 	args, err := docopt.ParseArgs(usage, argv, "")
@@ -91,23 +95,23 @@ Options:
 	}
 
 	app := safeGetString(args, "--app")
-	info := args["<key>=<value>"].([]string)
-
-	return cmdr.RegistrySet(app, info)
+	u := safeGetString(args, "<username>")
+	p := safeGetString(args, "<password>")
+	ptype := safeGetValue(args, "--ptype", "web")
+	return cmdr.RegistrySet(app, ptype, u, p)
 }
 
 func registryUnset(argv []string, cmdr cmd.Commander) error {
 	usage := `
 Unsets registry information for an application.
 
-Usage: drycc registry:unset <key>... [options]
-
-Arguments:
-  <key> the registry key to unset, for example: "username" or "password"
+Usage: drycc registry:unset [options]
 
 Options:
   -a --app=<app>
     the uniquely identifiable name for the application.
+  -p --ptype=<ptype>
+    the ptype for registry, default[web].
 `
 
 	args, err := docopt.ParseArgs(usage, argv, "")
@@ -117,7 +121,6 @@ Options:
 	}
 
 	app := safeGetString(args, "--app")
-	key := args["<key>"].([]string)
-
-	return cmdr.RegistryUnset(app, key)
+	ptype := safeGetValue(args, "--ptype", "web")
+	return cmdr.RegistryUnset(app, ptype)
 }
diff --git a/parser/registry_test.go b/parser/registry_test.go
index c1985267..6454c2bf 100644
--- a/parser/registry_test.go
+++ b/parser/registry_test.go
@@ -12,15 +12,15 @@ import (
 // Create fake implementations of each method that return the argument
 // we expect to have called the function (as an error to satisfy the interface).
 
-func (d FakeDryccCmd) RegistryList(string, int) error {
+func (d FakeDryccCmd) RegistryList(string, string, int) error {
 	return errors.New("registry:list")
 }
 
-func (d FakeDryccCmd) RegistrySet(string, []string) error {
+func (d FakeDryccCmd) RegistrySet(string, string, string, string) error {
 	return errors.New("registry:set")
 }
 
-func (d FakeDryccCmd) RegistryUnset(string, []string) error {
+func (d FakeDryccCmd) RegistryUnset(string, string) error {
 	return errors.New("registry:unset")
 }
 
@@ -46,11 +46,11 @@ func TestRegistry(t *testing.T) {
 			expected: "",
 		},
 		{
-			args:     []string{"registry:set", "username", "value"},
+			args:     []string{"registry:set", "username", "password"},
 			expected: "",
 		},
 		{
-			args:     []string{"registry:unset", "username"},
+			args:     []string{"registry:unset"},
 			expected: "",
 		},
 		{
diff --git a/parser/releases.go b/parser/releases.go
index f9b151a9..62b210ce 100644
--- a/parser/releases.go
+++ b/parser/releases.go
@@ -54,7 +54,7 @@ Usage: drycc releases:list [options]
 Options:
   -a --app=<app>
     the uniquely identifiable name for the application.
-  --ptypes=<ptypes>
+  -p --ptypes=<ptypes>
     the processes name as defined in your Procfile, comma separated.
   -l --limit=<num>
     the maximum number of results to display, defaults to config setting
diff --git a/parser/resources.go b/parser/resources.go
index 64e92c96..887646b5 100644
--- a/parser/resources.go
+++ b/parser/resources.go
@@ -194,8 +194,8 @@ Arguments:
 Options:
   -a --app=<app>
     the uniquely identifiable name for the application.
-  -l --limit=<num>
-    the maximum number of results to display, defaults to config setting
+  --details
+    show extra details provided to resource
 `
 
 	args, err := docopt.ParseArgs(usage, argv, "")
@@ -205,9 +205,10 @@ Options:
 	}
 
 	app := safeGetString(args, "--app")
+	details := safeGetBool(args, "--details")
 	name := safeGetString(args, "<name>")
 
-	return cmdr.ResourceGet(app, name)
+	return cmdr.ResourceGet(app, name, details)
 }
 
 func resourcePut(argv []string, cmdr cmd.Commander) error {
diff --git a/parser/resources_test.go b/parser/resources_test.go
index 204f8a2e..357a9635 100644
--- a/parser/resources_test.go
+++ b/parser/resources_test.go
@@ -28,7 +28,7 @@ func (d FakeDryccCmd) ResourcesList(string, int) error {
 	return errors.New("resources:list")
 }
 
-func (d FakeDryccCmd) ResourceGet(string, string) error {
+func (d FakeDryccCmd) ResourceGet(string, string, bool) error {
 	return errors.New("resources:describe")
 }
 
diff --git a/parser/tags.go b/parser/tags.go
index b215e338..ec62a432 100644
--- a/parser/tags.go
+++ b/parser/tags.go
@@ -52,7 +52,7 @@ Arguments:
 Options:
   -a --app=<app>
     the uniquely identifiable name of the application.
-  --version=<version>
+  -v --version=<version>
     the version for which the tag needs to be listed.
 `
 
diff --git a/parser/timeouts.go b/parser/timeouts.go
index 6060e1b4..07847dd6 100644
--- a/parser/timeouts.go
+++ b/parser/timeouts.go
@@ -48,7 +48,7 @@ Usage: drycc timeouts:list [options]
 Options:
   -a --app=<app>
     the uniquely identifiable name of the application.
-  --version=<version>
+  -v --version=<version>
     the version for which the timeout needs to be listed.
 `
 
diff --git a/parser/tokens.go b/parser/tokens.go
index 81c1bee0..557219f2 100644
--- a/parser/tokens.go
+++ b/parser/tokens.go
@@ -75,9 +75,9 @@ Arguments:
   provide a alias for controller authentication token.
 
 Options:
-  --username=<username>
+  -u --username=<username>
     provide a username for the account.
-  --password=<password>
+  -p --password=<password>
     provide a password for the account.
 `
 
