From fd684e5432014770e6cb8e6b2c8dac2be1d46ab8 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof Date: Fri, 22 Nov 2024 10:09:01 +0100 Subject: [PATCH] introduce marhsaller options and ability to include secrets content Signed-off-by: Nicolas De Loof --- loader/loader_test.go | 20 +++++++++------ loader/types_test.go | 3 +-- types/project.go | 60 ++++++++++++++++++++++++++++++++----------- types/project_test.go | 23 +++++++++++++++++ types/types.go | 30 +++++++++++++--------- 5 files changed, 99 insertions(+), 37 deletions(-) diff --git a/loader/loader_test.go b/loader/loader_test.go index 53186366..b223e213 100644 --- a/loader/loader_test.go +++ b/loader/loader_test.go @@ -912,7 +912,7 @@ networks: }, } - assert.DeepEqual(t, expected, config) + assertEqual(t, expected, config) } func TestUnsupportedProperties(t *testing.T) { @@ -1158,8 +1158,8 @@ func TestFullExample(t *testing.T) { assert.Check(t, is.DeepEqual(expectedConfig.Services, config.Services)) assert.Check(t, is.DeepEqual(expectedConfig.Networks, config.Networks)) assert.Check(t, is.DeepEqual(expectedConfig.Volumes, config.Volumes)) - assert.Check(t, is.DeepEqual(expectedConfig.Secrets, config.Secrets)) - assert.Check(t, is.DeepEqual(expectedConfig.Configs, config.Configs)) + assert.Check(t, is.DeepEqual(expectedConfig.Secrets, config.Secrets, cmpopts.IgnoreUnexported(types.SecretConfig{}))) + assert.Check(t, is.DeepEqual(expectedConfig.Configs, config.Configs, cmpopts.IgnoreUnexported(types.ConfigObjConfig{}))) assert.Check(t, is.DeepEqual(expectedConfig.Extensions, config.Extensions)) } @@ -1592,7 +1592,7 @@ secrets: External: true, }, } - assert.Check(t, is.DeepEqual(expected, project.Secrets)) + assert.Check(t, is.DeepEqual(expected, project.Secrets, cmpopts.IgnoreUnexported(types.SecretConfig{}))) assert.Check(t, is.Contains(buf.String(), "secrets.foo: external.name is deprecated. Please set name and external: true")) } @@ -1940,7 +1940,7 @@ secrets: "COMPOSE_PROJECT_NAME": "load-template-driver", }, } - assert.DeepEqual(t, config, expected, cmpopts.EquateEmpty()) + assertEqual(t, expected, config) } func TestLoadSecretDriver(t *testing.T) { @@ -2012,7 +2012,11 @@ secrets: "COMPOSE_PROJECT_NAME": "load-secret-driver", }, } - assert.DeepEqual(t, config, expected, cmpopts.EquateEmpty()) + assertEqual(t, config, expected) +} + +func assertEqual(t *testing.T, config *types.Project, expected *types.Project) { + assert.DeepEqual(t, config, expected, cmpopts.EquateEmpty(), cmpopts.IgnoreUnexported(types.SecretConfig{}), cmpopts.IgnoreUnexported(types.ConfigObjConfig{})) } func TestComposeFileWithVersion(t *testing.T) { @@ -3399,12 +3403,12 @@ secrets: "config": { Environment: "GA", Content: "BU", - }}) + }}, cmpopts.IgnoreUnexported(types.ConfigObjConfig{})) assert.DeepEqual(t, config.Secrets, types.Secrets{ "secret": { Environment: "MEU", Content: "Shadoks", - }}) + }}, cmpopts.IgnoreUnexported(types.SecretConfig{})) } func TestLoadDeviceMapping(t *testing.T) { diff --git a/loader/types_test.go b/loader/types_test.go index 8917ca10..38287266 100644 --- a/loader/types_test.go +++ b/loader/types_test.go @@ -17,7 +17,6 @@ package loader import ( - "encoding/json" "os" "testing" @@ -54,7 +53,7 @@ func TestJSONMarshalProject(t *testing.T) { project := fullExampleProject(workingDir, homeDir) expected := fullExampleJSON(workingDir, homeDir) - actual, err := json.MarshalIndent(project, "", " ") + actual, err := project.MarshalJSON() assert.NilError(t, err) assert.Check(t, is.Equal(expected, string(actual))) diff --git a/types/project.go b/types/project.go index 19d6e32b..fa4acc7c 100644 --- a/types/project.go +++ b/types/project.go @@ -560,39 +560,69 @@ func (p *Project) WithImagesResolved(resolver func(named reference.Named) (godig }) } +type marshallOptions struct { + secretsContent bool +} + +func WithSecretContent(o *marshallOptions) { + o.secretsContent = true +} + +func (opt *marshallOptions) apply(p *Project) *Project { + if opt.secretsContent { + p = p.deepCopy() + for name, config := range p.Secrets { + config.marshallContent = true + p.Secrets[name] = config + } + } + return p +} + +func applyMarshallOptions(p *Project, options ...func(*marshallOptions)) *Project { + opts := &marshallOptions{} + for _, option := range options { + option(opts) + } + p = opts.apply(p) + return p +} + // MarshalYAML marshal Project into a yaml tree -func (p *Project) MarshalYAML() ([]byte, error) { +func (p *Project) MarshalYAML(options ...func(*marshallOptions)) ([]byte, error) { buf := bytes.NewBuffer([]byte{}) encoder := yaml.NewEncoder(buf) encoder.SetIndent(2) // encoder.CompactSeqIndent() FIXME https://github.com/go-yaml/yaml/pull/753 - err := encoder.Encode(p) + src := applyMarshallOptions(p, options...) + err := encoder.Encode(src) if err != nil { return nil, err } return buf.Bytes(), nil } -// MarshalJSON makes Config implement json.Marshaler -func (p *Project) MarshalJSON() ([]byte, error) { +// MarshalJSON marshal Project into a json document +func (p *Project) MarshalJSON(options ...func(*marshallOptions)) ([]byte, error) { + src := applyMarshallOptions(p, options...) m := map[string]interface{}{ - "name": p.Name, - "services": p.Services, + "name": src.Name, + "services": src.Services, } - if len(p.Networks) > 0 { - m["networks"] = p.Networks + if len(src.Networks) > 0 { + m["networks"] = src.Networks } - if len(p.Volumes) > 0 { - m["volumes"] = p.Volumes + if len(src.Volumes) > 0 { + m["volumes"] = src.Volumes } - if len(p.Secrets) > 0 { - m["secrets"] = p.Secrets + if len(src.Secrets) > 0 { + m["secrets"] = src.Secrets } - if len(p.Configs) > 0 { - m["configs"] = p.Configs + if len(src.Configs) > 0 { + m["configs"] = src.Configs } - for k, v := range p.Extensions { + for k, v := range src.Extensions { m[k] = v } return json.MarshalIndent(m, "", " ") diff --git a/types/project_test.go b/types/project_test.go index c2c0fbfe..040f9360 100644 --- a/types/project_test.go +++ b/types/project_test.go @@ -20,6 +20,7 @@ import ( _ "crypto/sha256" "errors" "fmt" + "strings" "testing" "github.com/compose-spec/compose-go/v2/utils" @@ -410,3 +411,25 @@ func TestServicesWithCapabilities(t *testing.T) { assert.DeepEqual(t, []string{"service_1"}, gpu) assert.DeepEqual(t, []string{"service_1", "service_2"}, tpu) } + +func TestMarshallOptions(t *testing.T) { + p := &Project{ + Secrets: map[string]SecretConfig{ + "test": { + Name: "test", + Content: "SECRET", + File: "~/.secret", + }, + }, + } + yaml, err := p.MarshalYAML(WithSecretContent) + assert.NilError(t, err) + expected := ` +services: {} +secrets: + test: + name: test + file: ~/.secret + content: SECRET` + assert.Equal(t, strings.TrimSpace(string(yaml)), strings.TrimSpace(expected)) +} diff --git a/types/types.go b/types/types.go index 3cae5390..37749450 100644 --- a/types/types.go +++ b/types/types.go @@ -732,16 +732,18 @@ type CredentialSpecConfig struct { // FileObjectConfig is a config type for a file used by a service type FileObjectConfig struct { - Name string `yaml:"name,omitempty" json:"name,omitempty"` - File string `yaml:"file,omitempty" json:"file,omitempty"` - Environment string `yaml:"environment,omitempty" json:"environment,omitempty"` - Content string `yaml:"content,omitempty" json:"content,omitempty"` - External External `yaml:"external,omitempty" json:"external,omitempty"` - Labels Labels `yaml:"labels,omitempty" json:"labels,omitempty"` - Driver string `yaml:"driver,omitempty" json:"driver,omitempty"` - DriverOpts map[string]string `yaml:"driver_opts,omitempty" json:"driver_opts,omitempty"` - TemplateDriver string `yaml:"template_driver,omitempty" json:"template_driver,omitempty"` - Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"` + Name string `yaml:"name,omitempty" json:"name,omitempty"` + File string `yaml:"file,omitempty" json:"file,omitempty"` + Environment string `yaml:"environment,omitempty" json:"environment,omitempty"` + Content string `yaml:"content,omitempty" json:"content,omitempty"` + // configure marshalling to include Content - excluded by default to prevent sensitive data leaks + marshallContent bool + External External `yaml:"external,omitempty" json:"external,omitempty"` + Labels Labels `yaml:"labels,omitempty" json:"labels,omitempty"` + Driver string `yaml:"driver,omitempty" json:"driver,omitempty"` + DriverOpts map[string]string `yaml:"driver_opts,omitempty" json:"driver_opts,omitempty"` + TemplateDriver string `yaml:"template_driver,omitempty" json:"template_driver,omitempty"` + Extensions Extensions `yaml:"#extensions,inline,omitempty" json:"-"` } const ( @@ -775,14 +777,18 @@ type SecretConfig FileObjectConfig // MarshalYAML makes SecretConfig implement yaml.Marshaller func (s SecretConfig) MarshalYAML() (interface{}, error) { // secret content is set while loading model. Never marshall it - s.Content = "" + if !s.marshallContent { + s.Content = "" + } return FileObjectConfig(s), nil } // MarshalJSON makes SecretConfig implement json.Marshaller func (s SecretConfig) MarshalJSON() ([]byte, error) { // secret content is set while loading model. Never marshall it - s.Content = "" + if !s.marshallContent { + s.Content = "" + } return json.Marshal(FileObjectConfig(s)) }